Compare commits
20 Commits
bb642cab04
...
93a11fdbf3
| Author | SHA1 | Date |
|---|---|---|
|
|
93a11fdbf3 | |
|
|
e3cd6dc3a0 | |
|
|
1a60177fe4 | |
|
|
3c839a56bf | |
|
|
0d9ee4c40f | |
|
|
649ed5c6d7 | |
|
|
86dc961968 | |
|
|
d52d6c129b | |
|
|
d5b63d1c9b | |
|
|
e75889a127 | |
|
|
4b148ee823 | |
|
|
db08d9e6b9 | |
|
|
a757034d86 | |
|
|
28109eb63b | |
|
|
4c5e0330ef | |
|
|
ad7f350f00 | |
|
|
f01be49f6a | |
|
|
9d346a3d3a | |
|
|
de6c7a8008 | |
|
|
4aefb5be6a |
|
|
@ -26,6 +26,7 @@ export class EntityJoinController {
|
|||
sortOrder = "asc",
|
||||
enableEntityJoin = true,
|
||||
additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열)
|
||||
screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열)
|
||||
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
|
||||
...otherParams
|
||||
} = req.query;
|
||||
|
|
@ -65,6 +66,24 @@ export class EntityJoinController {
|
|||
}
|
||||
}
|
||||
|
||||
// 화면별 엔티티 설정 처리
|
||||
let parsedScreenEntityConfigs: Record<string, any> = {};
|
||||
if (screenEntityConfigs) {
|
||||
try {
|
||||
parsedScreenEntityConfigs =
|
||||
typeof screenEntityConfigs === "string"
|
||||
? JSON.parse(screenEntityConfigs)
|
||||
: screenEntityConfigs;
|
||||
logger.info(
|
||||
"화면별 엔티티 설정 파싱 완료:",
|
||||
parsedScreenEntityConfigs
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn("화면별 엔티티 설정 파싱 오류:", error);
|
||||
parsedScreenEntityConfigs = {};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await tableManagementService.getTableDataWithEntityJoins(
|
||||
tableName,
|
||||
{
|
||||
|
|
@ -79,6 +98,7 @@ export class EntityJoinController {
|
|||
enableEntityJoin:
|
||||
enableEntityJoin === "true" || enableEntityJoin === true,
|
||||
additionalJoinColumns: parsedAdditionalJoinColumns,
|
||||
screenEntityConfigs: parsedScreenEntityConfigs,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -348,14 +368,16 @@ export class EntityJoinController {
|
|||
);
|
||||
|
||||
// 현재 display_column으로 사용 중인 컬럼 제외
|
||||
const currentDisplayColumn =
|
||||
config.displayColumn || config.displayColumns[0];
|
||||
const availableColumns = columns.filter(
|
||||
(col) => col.columnName !== config.displayColumn
|
||||
(col) => col.columnName !== currentDisplayColumn
|
||||
);
|
||||
|
||||
return {
|
||||
joinConfig: config,
|
||||
tableName: config.referenceTable,
|
||||
currentDisplayColumn: config.displayColumn,
|
||||
currentDisplayColumn: currentDisplayColumn,
|
||||
availableColumns: availableColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.displayName || col.columnName,
|
||||
|
|
@ -373,7 +395,8 @@ export class EntityJoinController {
|
|||
return {
|
||||
joinConfig: config,
|
||||
tableName: config.referenceTable,
|
||||
currentDisplayColumn: config.displayColumn,
|
||||
currentDisplayColumn:
|
||||
config.displayColumn || config.displayColumns[0],
|
||||
availableColumns: [],
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@ const prisma = new PrismaClient();
|
|||
export class EntityJoinService {
|
||||
/**
|
||||
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
||||
* @param tableName 테이블명
|
||||
* @param screenEntityConfigs 화면별 엔티티 설정 (선택사항)
|
||||
*/
|
||||
async detectEntityJoins(tableName: string): Promise<EntityJoinConfig[]> {
|
||||
async detectEntityJoins(
|
||||
tableName: string,
|
||||
screenEntityConfigs?: Record<string, any>
|
||||
): Promise<EntityJoinConfig[]> {
|
||||
try {
|
||||
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
||||
|
||||
|
|
@ -37,9 +42,23 @@ export class EntityJoinService {
|
|||
},
|
||||
});
|
||||
|
||||
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
||||
entityColumns.forEach((col, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${col.column_name} -> ${col.reference_table}.${col.reference_column} (display: ${col.display_column})`
|
||||
);
|
||||
});
|
||||
|
||||
const joinConfigs: EntityJoinConfig[] = [];
|
||||
|
||||
for (const column of entityColumns) {
|
||||
logger.info(`🔍 Entity 컬럼 상세 정보:`, {
|
||||
column_name: column.column_name,
|
||||
reference_table: column.reference_table,
|
||||
reference_column: column.reference_column,
|
||||
display_column: column.display_column,
|
||||
});
|
||||
|
||||
if (
|
||||
!column.column_name ||
|
||||
!column.reference_table ||
|
||||
|
|
@ -48,8 +67,50 @@ export class EntityJoinService {
|
|||
continue;
|
||||
}
|
||||
|
||||
// display_column이 없으면 reference_column 사용
|
||||
const displayColumn = column.display_column || column.reference_column;
|
||||
// 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용
|
||||
const screenConfig = screenEntityConfigs?.[column.column_name];
|
||||
let displayColumns: string[] = [];
|
||||
let separator = " - ";
|
||||
|
||||
logger.info(`🔍 조건 확인 - 컬럼: ${column.column_name}`, {
|
||||
hasScreenConfig: !!screenConfig,
|
||||
hasDisplayColumns: screenConfig?.displayColumns,
|
||||
displayColumn: column.display_column,
|
||||
});
|
||||
|
||||
if (screenConfig && screenConfig.displayColumns) {
|
||||
// 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원)
|
||||
displayColumns = screenConfig.displayColumns;
|
||||
separator = screenConfig.separator || " - ";
|
||||
console.log(`🎯 화면별 엔티티 설정 적용: ${column.column_name}`, {
|
||||
displayColumns,
|
||||
separator,
|
||||
screenConfig,
|
||||
});
|
||||
} else if (column.display_column && column.display_column !== "none") {
|
||||
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
|
||||
displayColumns = [column.display_column];
|
||||
logger.info(
|
||||
`🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}`
|
||||
);
|
||||
} else {
|
||||
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
|
||||
// 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용
|
||||
let defaultDisplayColumn = column.reference_column;
|
||||
if (column.reference_table === "dept_info") {
|
||||
defaultDisplayColumn = "dept_name";
|
||||
} else if (column.reference_table === "company_info") {
|
||||
defaultDisplayColumn = "company_name";
|
||||
} else if (column.reference_table === "user_info") {
|
||||
defaultDisplayColumn = "user_name";
|
||||
}
|
||||
|
||||
displayColumns = [defaultDisplayColumn];
|
||||
logger.info(
|
||||
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})`
|
||||
);
|
||||
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
|
||||
}
|
||||
|
||||
// 별칭 컬럼명 생성 (writer -> writer_name)
|
||||
const aliasColumn = `${column.column_name}_name`;
|
||||
|
|
@ -59,17 +120,38 @@ export class EntityJoinService {
|
|||
sourceColumn: column.column_name,
|
||||
referenceTable: column.reference_table,
|
||||
referenceColumn: column.reference_column,
|
||||
displayColumn: displayColumn,
|
||||
displayColumns: displayColumns,
|
||||
displayColumn: displayColumns[0], // 하위 호환성
|
||||
aliasColumn: aliasColumn,
|
||||
separator: separator,
|
||||
};
|
||||
|
||||
logger.info(`🔧 기본 조인 설정 생성:`, {
|
||||
sourceTable: joinConfig.sourceTable,
|
||||
sourceColumn: joinConfig.sourceColumn,
|
||||
referenceTable: joinConfig.referenceTable,
|
||||
aliasColumn: joinConfig.aliasColumn,
|
||||
displayColumns: joinConfig.displayColumns,
|
||||
});
|
||||
|
||||
// 조인 설정 유효성 검증
|
||||
logger.info(
|
||||
`🔍 조인 설정 검증 중: ${joinConfig.sourceColumn} -> ${joinConfig.referenceTable}`
|
||||
);
|
||||
if (await this.validateJoinConfig(joinConfig)) {
|
||||
joinConfigs.push(joinConfig);
|
||||
logger.info(`✅ 조인 설정 추가됨: ${joinConfig.aliasColumn}`);
|
||||
} else {
|
||||
logger.warn(`❌ 조인 설정 검증 실패: ${joinConfig.sourceColumn}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`);
|
||||
logger.info(`🎯 Entity 조인 설정 생성 완료: ${joinConfigs.length}개`);
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable}.${config.referenceColumn} AS ${config.aliasColumn}`
|
||||
);
|
||||
});
|
||||
return joinConfigs;
|
||||
} catch (error) {
|
||||
logger.error(`Entity 조인 감지 실패: ${tableName}`, error);
|
||||
|
|
@ -90,8 +172,10 @@ export class EntityJoinService {
|
|||
offset?: number
|
||||
): { query: string; aliasMap: Map<string, string> } {
|
||||
try {
|
||||
// 기본 SELECT 컬럼들
|
||||
const baseColumns = selectColumns.map((col) => `main.${col}`).join(", ");
|
||||
// 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지)
|
||||
const baseColumns = selectColumns
|
||||
.map((col) => `main.${col}::TEXT AS ${col}`)
|
||||
.join(", ");
|
||||
|
||||
// Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리)
|
||||
// 별칭 매핑 생성 (JOIN 절과 동일한 로직)
|
||||
|
|
@ -130,10 +214,69 @@ export class EntityJoinService {
|
|||
});
|
||||
|
||||
const joinColumns = joinConfigs
|
||||
.map(
|
||||
(config) =>
|
||||
`COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}`
|
||||
)
|
||||
.map((config) => {
|
||||
const alias = aliasMap.get(config.referenceTable);
|
||||
const displayColumns = config.displayColumns || [
|
||||
config.displayColumn,
|
||||
];
|
||||
const separator = config.separator || " - ";
|
||||
|
||||
if (displayColumns.length === 1) {
|
||||
// 단일 컬럼인 경우
|
||||
const col = displayColumns[0];
|
||||
const isJoinTableColumn = [
|
||||
"dept_name",
|
||||
"dept_code",
|
||||
"master_user_id",
|
||||
"location_name",
|
||||
"parent_dept_code",
|
||||
"master_sabun",
|
||||
"location",
|
||||
"data_type",
|
||||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||
} else {
|
||||
return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||
}
|
||||
} else {
|
||||
// 여러 컬럼인 경우 CONCAT으로 연결
|
||||
// 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리
|
||||
const concatParts = displayColumns
|
||||
.map((col) => {
|
||||
// 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용)
|
||||
// 현재는 dept_info 테이블의 컬럼들을 확인
|
||||
const isJoinTableColumn = [
|
||||
"dept_name",
|
||||
"dept_code",
|
||||
"master_user_id",
|
||||
"location_name",
|
||||
"parent_dept_code",
|
||||
"master_sabun",
|
||||
"location",
|
||||
"data_type",
|
||||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
// 조인 테이블 컬럼은 조인 별칭 사용
|
||||
return `COALESCE(${alias}.${col}::TEXT, '')`;
|
||||
} else {
|
||||
// 기본 테이블 컬럼은 main 별칭 사용
|
||||
return `COALESCE(main.${col}::TEXT, '')`;
|
||||
}
|
||||
})
|
||||
.join(` || '${separator}' || `);
|
||||
|
||||
return `(${concatParts}) AS ${config.aliasColumn}`;
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// SELECT 절 구성
|
||||
|
|
@ -179,7 +322,7 @@ export class EntityJoinService {
|
|||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
logger.debug(`생성된 Entity 조인 쿼리:`, query);
|
||||
logger.info(`🔍 생성된 Entity 조인 쿼리:`, query);
|
||||
return {
|
||||
query: query,
|
||||
aliasMap: aliasMap,
|
||||
|
|
@ -199,11 +342,28 @@ export class EntityJoinService {
|
|||
try {
|
||||
const strategies = await Promise.all(
|
||||
joinConfigs.map(async (config) => {
|
||||
// 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가
|
||||
if (config.displayColumns && config.displayColumns.length > 1) {
|
||||
console.log(
|
||||
`🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${config.sourceColumn}`,
|
||||
config.displayColumns
|
||||
);
|
||||
return "join";
|
||||
}
|
||||
|
||||
// 참조 테이블의 캐시 가능성 확인
|
||||
const displayCol =
|
||||
config.displayColumn ||
|
||||
config.displayColumns?.[0] ||
|
||||
config.referenceColumn;
|
||||
logger.info(
|
||||
`🔍 캐시 확인용 표시 컬럼: ${config.referenceTable} - ${displayCol}`
|
||||
);
|
||||
|
||||
const cachedData = await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn
|
||||
displayCol
|
||||
);
|
||||
|
||||
return cachedData ? "cache" : "join";
|
||||
|
|
@ -233,6 +393,14 @@ export class EntityJoinService {
|
|||
*/
|
||||
private async validateJoinConfig(config: EntityJoinConfig): Promise<boolean> {
|
||||
try {
|
||||
logger.info("🔍 조인 설정 검증 상세:", {
|
||||
sourceColumn: config.sourceColumn,
|
||||
referenceTable: config.referenceTable,
|
||||
displayColumns: config.displayColumns,
|
||||
displayColumn: config.displayColumn,
|
||||
aliasColumn: config.aliasColumn,
|
||||
});
|
||||
|
||||
// 참조 테이블 존재 확인
|
||||
const tableExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.tables
|
||||
|
|
@ -245,19 +413,34 @@ export class EntityJoinService {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 참조 컬럼 존재 확인
|
||||
const columnExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = ${config.referenceTable}
|
||||
AND column_name = ${config.displayColumn}
|
||||
LIMIT 1
|
||||
`;
|
||||
// 참조 컬럼 존재 확인 (displayColumns[0] 사용)
|
||||
const displayColumn = config.displayColumns?.[0] || config.displayColumn;
|
||||
logger.info(
|
||||
`🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})`
|
||||
);
|
||||
|
||||
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||||
logger.warn(
|
||||
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}`
|
||||
// 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용
|
||||
if (displayColumn && displayColumn !== "none") {
|
||||
const columnExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = ${config.referenceTable}
|
||||
AND column_name = ${displayColumn}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||||
logger.warn(
|
||||
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.info(
|
||||
`✅ 표시 컬럼 확인 완료: ${config.referenceTable}.${displayColumn}`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -2023,6 +2023,7 @@ export class TableManagementService {
|
|||
sourceColumn: string;
|
||||
joinAlias: string;
|
||||
}>;
|
||||
screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
|
||||
}
|
||||
): Promise<EntityJoinResponse> {
|
||||
const startTime = Date.now();
|
||||
|
|
@ -2042,8 +2043,22 @@ export class TableManagementService {
|
|||
};
|
||||
}
|
||||
|
||||
// Entity 조인 설정 감지
|
||||
let joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
||||
// Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
|
||||
let joinConfigs = await entityJoinService.detectEntityJoins(
|
||||
tableName,
|
||||
options.screenEntityConfigs
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정`
|
||||
);
|
||||
if (joinConfigs.length > 0) {
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` 조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
|
||||
if (
|
||||
|
|
@ -2053,32 +2068,84 @@ export class TableManagementService {
|
|||
logger.info(
|
||||
`추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개`
|
||||
);
|
||||
logger.info(
|
||||
"📋 전달받은 additionalJoinColumns:",
|
||||
options.additionalJoinColumns
|
||||
);
|
||||
|
||||
for (const additionalColumn of options.additionalJoinColumns) {
|
||||
// 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기
|
||||
// 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기)
|
||||
const baseJoinConfig = joinConfigs.find(
|
||||
(config) => config.referenceTable === additionalColumn.sourceTable
|
||||
(config) => config.sourceColumn === additionalColumn.sourceColumn
|
||||
);
|
||||
|
||||
if (baseJoinConfig) {
|
||||
// joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name)
|
||||
// sourceColumn을 제거한 나머지 부분이 실제 컬럼명
|
||||
const sourceColumn = baseJoinConfig.sourceColumn; // dept_code
|
||||
const joinAlias = additionalColumn.joinAlias; // dept_code_company_name
|
||||
const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name
|
||||
|
||||
logger.info(`🔍 조인 컬럼 상세 분석:`, {
|
||||
sourceColumn,
|
||||
joinAlias,
|
||||
actualColumnName,
|
||||
referenceTable: additionalColumn.sourceTable,
|
||||
});
|
||||
|
||||
// 🚨 기본 Entity 조인과 중복되지 않도록 체크
|
||||
const isBasicEntityJoin =
|
||||
additionalColumn.joinAlias ===
|
||||
`${baseJoinConfig.sourceColumn}_name`;
|
||||
|
||||
if (isBasicEntityJoin) {
|
||||
logger.info(
|
||||
`⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀`
|
||||
);
|
||||
continue; // 기본 Entity 조인과 중복되면 추가하지 않음
|
||||
}
|
||||
|
||||
// 추가 조인 컬럼 설정 생성
|
||||
const additionalJoinConfig: EntityJoinConfig = {
|
||||
sourceTable: tableName,
|
||||
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer)
|
||||
referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info)
|
||||
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id)
|
||||
displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email)
|
||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email)
|
||||
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code)
|
||||
referenceTable:
|
||||
(additionalColumn as any).referenceTable ||
|
||||
baseJoinConfig.referenceTable, // 참조 테이블 (dept_info)
|
||||
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code)
|
||||
displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name)
|
||||
displayColumn: actualColumnName, // 하위 호환성
|
||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name)
|
||||
separator: " - ", // 기본 구분자
|
||||
};
|
||||
|
||||
joinConfigs.push(additionalJoinConfig);
|
||||
logger.info(
|
||||
`추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn}`
|
||||
`✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
|
||||
);
|
||||
logger.info(`🔍 추가된 조인 설정 상세:`, {
|
||||
sourceTable: additionalJoinConfig.sourceTable,
|
||||
sourceColumn: additionalJoinConfig.sourceColumn,
|
||||
referenceTable: additionalJoinConfig.referenceTable,
|
||||
displayColumns: additionalJoinConfig.displayColumns,
|
||||
aliasColumn: additionalJoinConfig.aliasColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 조인 설정 배열 로깅
|
||||
logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`);
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`,
|
||||
{
|
||||
displayColumns: config.displayColumns,
|
||||
displayColumn: config.displayColumn,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (joinConfigs.length === 0) {
|
||||
logger.info(`Entity 조인 설정이 없음: ${tableName}`);
|
||||
const basicResult = await this.getTableData(tableName, options);
|
||||
|
|
@ -2092,8 +2159,21 @@ export class TableManagementService {
|
|||
}
|
||||
|
||||
// 조인 전략 결정 (테이블 크기 기반)
|
||||
const strategy =
|
||||
await entityJoinService.determineJoinStrategy(joinConfigs);
|
||||
// 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장)
|
||||
let strategy: "full_join" | "cache_lookup" | "hybrid";
|
||||
|
||||
if (
|
||||
options.additionalJoinColumns &&
|
||||
options.additionalJoinColumns.length > 0
|
||||
) {
|
||||
strategy = "full_join";
|
||||
console.log(
|
||||
`🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)`
|
||||
);
|
||||
} else {
|
||||
strategy = await entityJoinService.determineJoinStrategy(joinConfigs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)`
|
||||
);
|
||||
|
|
@ -2239,10 +2319,18 @@ export class TableManagementService {
|
|||
try {
|
||||
// 캐시 데이터 미리 로드
|
||||
for (const config of joinConfigs) {
|
||||
const displayCol =
|
||||
config.displayColumn ||
|
||||
config.displayColumns?.[0] ||
|
||||
config.referenceColumn;
|
||||
logger.info(
|
||||
`🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}`
|
||||
);
|
||||
|
||||
await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn
|
||||
displayCol
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2429,7 +2517,7 @@ export class TableManagementService {
|
|||
const lookupValue = referenceCacheService.getLookupValue(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn,
|
||||
config.displayColumn || config.displayColumns[0],
|
||||
String(sourceValue)
|
||||
);
|
||||
|
||||
|
|
@ -2723,7 +2811,7 @@ export class TableManagementService {
|
|||
const cachedData = await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn
|
||||
config.displayColumn || config.displayColumns[0]
|
||||
);
|
||||
|
||||
if (cachedData && cachedData.size > 0) {
|
||||
|
|
@ -2807,7 +2895,7 @@ export class TableManagementService {
|
|||
const cachedData = await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn
|
||||
config.displayColumn || config.displayColumns[0]
|
||||
);
|
||||
|
||||
if (cachedData) {
|
||||
|
|
@ -2846,7 +2934,7 @@ export class TableManagementService {
|
|||
const hitRate = referenceCacheService.getCacheHitRate(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn
|
||||
config.displayColumn || config.displayColumns[0]
|
||||
);
|
||||
totalHitRate += hitRate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,8 +77,10 @@ export interface EntityJoinConfig {
|
|||
sourceColumn: string; // writer
|
||||
referenceTable: string; // user_info
|
||||
referenceColumn: string; // user_id (조인 키)
|
||||
displayColumn: string; // user_name (표시할 값)
|
||||
displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들)
|
||||
displayColumn?: string; // user_name (하위 호환성용, deprecated)
|
||||
aliasColumn: string; // writer_name (결과 컬럼명)
|
||||
separator?: string; // ' - ' (여러 컬럼 연결 시 구분자)
|
||||
}
|
||||
|
||||
export interface EntityJoinResponse {
|
||||
|
|
|
|||
|
|
@ -185,11 +185,12 @@ export default function BatchManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">배치 관리</h1>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">배치 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.
|
||||
</p>
|
||||
|
|
@ -428,6 +429,7 @@ export default function BatchManagementPage() {
|
|||
onSave={handleModalSave}
|
||||
job={selectedJob}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,12 @@ export default function CollectionManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">수집 관리</h1>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">수집 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.
|
||||
</p>
|
||||
|
|
@ -332,6 +333,7 @@ export default function CollectionManagementPage() {
|
|||
onSave={handleModalSave}
|
||||
config={selectedConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,22 +11,23 @@ export default function CommonCodeManagementPage() {
|
|||
const { selectedCategoryCode, selectCategory } = useSelectedCategory();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">공통코드 관리</h1>
|
||||
<p className="text-muted-foreground">시스템에서 사용하는 공통코드를 관리합니다</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">공통코드 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템에서 사용하는 공통코드를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
{/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */}
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:gap-8">
|
||||
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
|
||||
<div className="w-full lg:w-80 lg:flex-shrink-0">
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<Card className="h-full shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">📂 코드 카테고리</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
|
|
@ -37,8 +38,8 @@ export default function CommonCodeManagementPage() {
|
|||
|
||||
{/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<Card className="h-fit shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
📋 코드 상세 정보
|
||||
{selectedCategoryCode && (
|
||||
|
|
@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
|
|||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement";
|
|||
* 회사 관리 페이지
|
||||
*/
|
||||
export default function CompanyPage() {
|
||||
return <CompanyManagement />;
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">회사 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템에서 사용하는 회사 정보를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<CompanyManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,48 +76,49 @@ export default function DataFlowPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{currentStep !== "list" && (
|
||||
<Button variant="outline" size="sm" onClick={goToPreviousStep} className="flex items-center">
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="flex items-center text-2xl font-bold text-gray-900">
|
||||
<span className="mr-2">{stepConfig[currentStep].icon}</span>
|
||||
{stepConfig[currentStep].title}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">{stepConfig[currentStep].description}</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">데이터 흐름 관리</h1>
|
||||
<p className="mt-2 text-gray-600">테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다</p>
|
||||
</div>
|
||||
{currentStep !== "list" && (
|
||||
<Button variant="outline" onClick={goToPreviousStep} className="flex items-center shadow-sm">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="h-full p-6">
|
||||
<DataFlowList onDesignDiagram={handleDesignDiagram} />
|
||||
{/* 단계별 내용 */}
|
||||
<div className="space-y-6">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
</div>
|
||||
<DataFlowList onDesignDiagram={handleDesignDiagram} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 관계도 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="h-full">
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.company_code || "COMP001"}
|
||||
onSave={handleSave}
|
||||
selectedDiagram={null}
|
||||
onBackToList={() => goToStep("list")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 관계도 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
|
||||
</div>
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.company_code || "COMP001"}
|
||||
onSave={handleSave}
|
||||
selectedDiagram={null}
|
||||
onBackToList={() => goToStep("list")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() {
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,14 +220,18 @@ export default function ExternalConnectionsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 text-2xl font-bold text-gray-900">외부 커넥션 관리</h1>
|
||||
<p className="text-gray-600">외부 데이터베이스 연결 정보를 관리합니다.</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">외부 커넥션 관리</h1>
|
||||
<p className="mt-2 text-gray-600">외부 데이터베이스 연결 정보를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
<Card className="mb-6">
|
||||
<Card className="mb-6 shadow-sm">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
|
|
@ -285,7 +289,7 @@ export default function ExternalConnectionsPage() {
|
|||
<div className="text-gray-500">로딩 중...</div>
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<Card>
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="pt-6">
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
|
|
@ -298,7 +302,7 @@ export default function ExternalConnectionsPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
|
@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() {
|
|||
connectionName={selectedConnection.connection_name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
import MultiLang from "@/components/admin/MultiLang";
|
||||
|
||||
export default function I18nPage() {
|
||||
return <MultiLang />;
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6">
|
||||
<MultiLang />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,19 +220,21 @@ export default function LayoutManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">레이아웃 관리</h1>
|
||||
<p className="text-gray-600">화면 레이아웃을 생성하고 관리합니다.</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">레이아웃 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 레이아웃을 생성하고 관리합니다</p>
|
||||
</div>
|
||||
<Button className="flex items-center gap-2 shadow-sm" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="h-4 w-4" />새 레이아웃
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="flex items-center gap-2" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="h-4 w-4" />새 레이아웃
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
<Card className="mb-6">
|
||||
<Card className="mb-6 shadow-sm">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
|
|
@ -282,7 +284,7 @@ export default function LayoutManagementPage() {
|
|||
{layouts.map((layout) => {
|
||||
const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS];
|
||||
return (
|
||||
<Card key={layout.layoutCode} className="transition-shadow hover:shadow-lg">
|
||||
<Card key={layout.layoutCode} className="shadow-sm transition-shadow hover:shadow-lg">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -411,6 +413,7 @@ export default function LayoutManagementPage() {
|
|||
loadCategoryCounts();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement";
|
|||
|
||||
export default function MenuPage() {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<MenuManagement />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">메뉴 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템 메뉴를 관리하고 화면을 할당합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<MenuManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard";
|
|||
|
||||
export default function MonitoringPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">모니터링</h1>
|
||||
<p className="text-muted-foreground">
|
||||
배치 작업 실행 상태를 실시간으로 모니터링합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">모니터링</h1>
|
||||
<p className="text-muted-foreground">
|
||||
배치 작업 실행 상태를 실시간으로 모니터링합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 모니터링 대시보드 */}
|
||||
<MonitoringDashboard />
|
||||
{/* 모니터링 대시보드 */}
|
||||
<MonitoringDashboard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import Link from "next/link";
|
|||
*/
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50/30 p-8 space-y-8">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 관리자 기능 카드들 */}
|
||||
<div className="mx-auto max-w-7xl grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/admin/userMng" className="block">
|
||||
|
|
@ -162,6 +163,7 @@ export default function AdminPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,18 +66,27 @@ export default function ScreenManagementPage() {
|
|||
const isLastStep = currentStep === "template";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 화면 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="h-full p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{stepConfig.list.title}</h2>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToNextStep("design")}>
|
||||
화면 설계하기 <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">화면 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면을 설계하고 템플릿을 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 화면 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToNextStep("design")}>
|
||||
화면 설계하기 <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScreenList
|
||||
onScreenSelect={setSelectedScreen}
|
||||
selectedScreen={selectedScreen}
|
||||
|
|
@ -89,31 +98,38 @@ export default function ScreenManagementPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 화면 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="h-full">
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 템플릿 관리 단계 */}
|
||||
{currentStep === "template" && (
|
||||
<div className="h-full p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{stepConfig.template.title}</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goToPreviousStep}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전 단계
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToStep("list")}>
|
||||
목록으로 돌아가기
|
||||
{/* 화면 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
|
||||
<Button variant="outline" className="shadow-sm" onClick={() => goToStep("list")}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />목록으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 템플릿 관리 단계 */}
|
||||
{currentStep === "template" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.template.title}</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="shadow-sm" onClick={goToPreviousStep}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전 단계
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToStep("list")}>
|
||||
목록으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ export default function EditWebTypePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<Link href={`/admin/standards/${webType}`}>
|
||||
|
|
@ -502,6 +503,7 @@ export default function EditWebTypePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ export default function WebTypeDetailPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -280,6 +281,7 @@ export default function WebTypeDetailPage() {
|
|||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ export default function NewWebTypePage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<Link href="/admin/standards">
|
||||
|
|
@ -453,6 +454,7 @@ export default function NewWebTypePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,46 +127,47 @@ export default function WebTypesManagePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">웹타입 관리</h1>
|
||||
<p className="text-muted-foreground">화면관리에서 사용할 웹타입들을 관리합니다.</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">웹타입 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
||||
</div>
|
||||
<Link href="/admin/standards/new">
|
||||
<Button className="shadow-sm">
|
||||
<Plus className="mr-2 h-4 w-4" />새 웹타입 추가
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href="/admin/standards/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />새 웹타입 추가
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Filter className="h-5 w-5" />
|
||||
필터 및 검색
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-3 left-3 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="웹타입명, 설명 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
{/* 필터 및 검색 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Filter className="h-5 w-5 text-gray-600" />
|
||||
필터 및 검색
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="웹타입명, 설명 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 필터 */}
|
||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="카테고리 선택" />
|
||||
</SelectTrigger>
|
||||
{/* 카테고리 필터 */}
|
||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="카테고리 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 카테고리</SelectItem>
|
||||
{categories.map((category) => (
|
||||
|
|
@ -177,96 +178,96 @@ export default function WebTypesManagePage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 활성화 상태 필터 */}
|
||||
<Select value={activeFilter} onValueChange={setActiveFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="Y">활성화</SelectItem>
|
||||
<SelectItem value="N">비활성화</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 활성화 상태 필터 */}
|
||||
<Select value={activeFilter} onValueChange={setActiveFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="Y">활성화</SelectItem>
|
||||
<SelectItem value="N">비활성화</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 초기화 버튼 */}
|
||||
<Button variant="outline" onClick={resetFilters}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
초기화
|
||||
</Button>
|
||||
{/* 초기화 버튼 */}
|
||||
<Button variant="outline" onClick={resetFilters}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 결과 통계 */}
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground text-sm">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
||||
</div>
|
||||
{/* 결과 통계 */}
|
||||
<div className="bg-white rounded-lg border px-4 py-3">
|
||||
<p className="text-gray-700 text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
||||
</div>
|
||||
|
||||
{/* 웹타입 목록 테이블 */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
|
||||
<div className="flex items-center gap-2">
|
||||
순서
|
||||
{sortField === "sort_order" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입 코드
|
||||
{sortField === "web_type" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입명
|
||||
{sortField === "type_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
|
||||
<div className="flex items-center gap-2">
|
||||
카테고리
|
||||
{sortField === "category" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>설명</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
연결된 컴포넌트
|
||||
{sortField === "component_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
|
||||
<div className="flex items-center gap-2">
|
||||
설정 패널
|
||||
{sortField === "config_panel" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
|
||||
<div className="flex items-center gap-2">
|
||||
상태
|
||||
{sortField === "is_active" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
|
||||
<div className="flex items-center gap-2">
|
||||
최종 수정일
|
||||
{sortField === "updated_date" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">작업</TableHead>
|
||||
{/* 웹타입 목록 테이블 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
|
||||
<div className="flex items-center gap-2">
|
||||
순서
|
||||
{sortField === "sort_order" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입 코드
|
||||
{sortField === "web_type" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입명
|
||||
{sortField === "type_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
|
||||
<div className="flex items-center gap-2">
|
||||
카테고리
|
||||
{sortField === "category" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>설명</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
연결된 컴포넌트
|
||||
{sortField === "component_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
|
||||
<div className="flex items-center gap-2">
|
||||
설정 패널
|
||||
{sortField === "config_panel" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
|
||||
<div className="flex items-center gap-2">
|
||||
상태
|
||||
{sortField === "is_active" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
|
||||
<div className="flex items-center gap-2">
|
||||
최종 수정일
|
||||
{sortField === "updated_date" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -309,24 +310,24 @@ export default function WebTypesManagePage() {
|
|||
<TableCell className="text-muted-foreground text-sm">
|
||||
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/admin/standards/${webType.web_type}`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/admin/standards/${webType.web_type}/edit`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/admin/standards/${webType.web_type}`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/admin/standards/${webType.web_type}/edit`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>웹타입 삭제</AlertDialogTitle>
|
||||
|
|
@ -364,6 +365,7 @@ export default function WebTypesManagePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,9 +541,9 @@ export default function TableManagementPage() {
|
|||
}, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-none space-y-6 p-6">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
|
||||
|
|
@ -593,10 +593,10 @@ export default function TableManagementPage() {
|
|||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
|
||||
{/* 테이블 목록 */}
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<Card className="lg:col-span-1 shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
<Database className="h-5 w-5 text-gray-600" />
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -663,10 +663,10 @@ export default function TableManagementPage() {
|
|||
</Card>
|
||||
|
||||
{/* 컬럼 타입 관리 */}
|
||||
<Card className="lg:col-span-4">
|
||||
<CardHeader>
|
||||
<Card className="lg:col-span-4 shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
<Settings className="h-5 w-5 text-gray-600" />
|
||||
{selectedTable ? <>테이블 설정 - {selectedTable}</> : "테이블 타입 관리"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -866,47 +866,6 @@ export default function TableManagementPage() {
|
|||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 표시 컬럼 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && (
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-gray-600">표시 컬럼</label>
|
||||
<Select
|
||||
value={column.displayColumn || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(
|
||||
column.columnName,
|
||||
"entity_display_column",
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 bg-white text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
||||
<SelectItem
|
||||
key={`display-col-${refCol.columnName}-${index}`}
|
||||
value={refCol.columnName}
|
||||
>
|
||||
<span className="font-medium">{refCol.columnName}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{(!referenceTableColumns[column.referenceTable] ||
|
||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
||||
<SelectItem value="loading" disabled>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-3 animate-spin rounded-full border border-blue-500 border-t-transparent"></div>
|
||||
로딩중
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설정 완료 표시 - 간소화 */}
|
||||
|
|
|
|||
|
|
@ -145,27 +145,28 @@ export default function TemplatesManagePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">템플릿 관리</h1>
|
||||
<p className="text-muted-foreground">화면 디자이너에서 사용할 템플릿을 관리합니다.</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">템플릿 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button asChild className="shadow-sm">
|
||||
<Link href="/admin/templates/new">
|
||||
<Plus className="mr-2 h-4 w-4" />새 템플릿
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button asChild>
|
||||
<Link href="/admin/templates/new">
|
||||
<Plus className="mr-2 h-4 w-4" />새 템플릿
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center">
|
||||
<Filter className="mr-2 h-5 w-5" />
|
||||
<Filter className="mr-2 h-5 w-5 text-gray-600" />
|
||||
필터 및 검색
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -230,8 +231,8 @@ export default function TemplatesManagePage() {
|
|||
</Card>
|
||||
|
||||
{/* 템플릿 목록 테이블 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle>템플릿 목록 ({filteredAndSortedTemplates.length}개)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -390,6 +391,7 @@ export default function TemplatesManagePage() {
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement";
|
|||
*/
|
||||
export default function UserMngPage() {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<UserManagement />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">사용자 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템 사용자 계정 및 권한을 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<UserManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: true,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: true,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
},
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: false,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: false,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
},
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ export default function ScreenViewPage() {
|
|||
const labelText = component.style?.labelText || component.label || "";
|
||||
const labelStyle = {
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
padding: component.style?.labelPadding || "0",
|
||||
|
|
|
|||
|
|
@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => {
|
|||
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
|
||||
<div className="w-[20%] border-r bg-gray-50">
|
||||
<div className="p-6">
|
||||
<h2 className="mb-4 text-lg font-semibold">{getUITextSync("menu.type.title")}</h2>
|
||||
<div className="space-y-3">
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50 pb-3">
|
||||
<CardTitle className="text-lg">{getUITextSync("menu.type.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-4">
|
||||
<Card
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
||||
|
|
@ -864,21 +867,23 @@ export const MenuManagement: React.FC = () => {
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
|
||||
<div className="w-[80%] overflow-hidden">
|
||||
<div className="flex h-full flex-col p-6">
|
||||
<div className="mb-6 flex-shrink-0">
|
||||
<h2 className="mb-2 text-xl font-semibold">
|
||||
{getMenuTypeString()} {getUITextSync("menu.list.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 필터 영역 */}
|
||||
<div className="mb-4 flex-shrink-0">
|
||||
<Card className="flex-1 shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="text-xl">
|
||||
{getMenuTypeString()} {getUITextSync("menu.list.title")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
{/* 검색 및 필터 영역 */}
|
||||
<div className="mb-4 flex-shrink-0">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div>
|
||||
<Label htmlFor="company">{getUITextSync("filter.company")}</Label>
|
||||
|
|
@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
|
||||
{getUITextSync("button.add.top.level")}
|
||||
</Button>
|
||||
{selectedMenus.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelectedMenus}
|
||||
disabled={deleting}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{deleting ? (
|
||||
<>
|
||||
<LoadingSpinner size="sm" className="mr-2" />
|
||||
{getUITextSync("button.delete.processing")}
|
||||
</>
|
||||
) : (
|
||||
getUITextSync("button.delete.selected.count", {
|
||||
count: selectedMenus.size,
|
||||
})
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
|
||||
{getUITextSync("button.add.top.level")}
|
||||
</Button>
|
||||
{selectedMenus.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelectedMenus}
|
||||
disabled={deleting}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{deleting ? (
|
||||
<>
|
||||
<LoadingSpinner size="sm" className="mr-2" />
|
||||
{getUITextSync("button.delete.processing")}
|
||||
</>
|
||||
) : (
|
||||
getUITextSync("button.delete.selected.count", {
|
||||
count: selectedMenus.size,
|
||||
})
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<MenuTable
|
||||
menus={getCurrentMenus()}
|
||||
title=""
|
||||
onAddMenu={handleAddMenu}
|
||||
onEditMenu={handleEditMenu}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
selectedMenus={selectedMenus}
|
||||
onMenuSelectionChange={handleMenuSelectionChange}
|
||||
onSelectAllMenus={handleSelectAllMenus}
|
||||
expandedMenus={expandedMenus}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
uiTexts={uiTexts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MenuTable
|
||||
menus={getCurrentMenus()}
|
||||
title=""
|
||||
onAddMenu={handleAddMenu}
|
||||
onEditMenu={handleEditMenu}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
selectedMenus={selectedMenus}
|
||||
onMenuSelectionChange={handleMenuSelectionChange}
|
||||
onSelectAllMenus={handleSelectAllMenus}
|
||||
expandedMenus={expandedMenus}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
uiTexts={uiTexts}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => {
|
|||
</TabsContent>
|
||||
|
||||
{/* 화면 할당 탭 */}
|
||||
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden">
|
||||
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
|
||||
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden p-6">
|
||||
<Card className="h-full shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle>화면 할당</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full overflow-hidden">
|
||||
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState, Suspense } from "react";
|
||||
import { useState, Suspense, useEffect } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
const searchParams = useSearchParams();
|
||||
const { user, logout, refreshUserData } = useAuth();
|
||||
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
// 화면 크기 감지 및 사이드바 초기 상태 설정
|
||||
useEffect(() => {
|
||||
const checkIsMobile = () => {
|
||||
const mobile = window.innerWidth < 1024; // lg 브레이크포인트
|
||||
setIsMobile(mobile);
|
||||
// 모바일에서만 사이드바를 닫음
|
||||
if (mobile) {
|
||||
setSidebarOpen(false);
|
||||
} else {
|
||||
setSidebarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkIsMobile();
|
||||
window.addEventListener('resize', checkIsMobile);
|
||||
return () => window.removeEventListener('resize', checkIsMobile);
|
||||
}, []);
|
||||
|
||||
// 프로필 관련 로직
|
||||
const {
|
||||
|
|
@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
? `/screens/${firstScreen.screenId}?mode=admin`
|
||||
: `/screens/${firstScreen.screenId}`;
|
||||
|
||||
console.log("🎯 메뉴에서 화면으로 이동:", {
|
||||
menuName: menu.name,
|
||||
screenId: firstScreen.screenId,
|
||||
isAdminMode,
|
||||
targetPath: screenPath,
|
||||
});
|
||||
|
||||
router.push(screenPath);
|
||||
setSidebarOpen(false);
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
|
||||
if (menu.url && menu.url !== "#") {
|
||||
router.push(menu.url);
|
||||
setSidebarOpen(false);
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
} else {
|
||||
// URL도 없고 할당된 화면도 없으면 경고 메시지
|
||||
console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu);
|
||||
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
||||
}
|
||||
}
|
||||
|
|
@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
await logout();
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error("로그아웃 실패:", error);
|
||||
// 로그아웃 실패 시 처리
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
return (
|
||||
<div key={menu.id}>
|
||||
<div
|
||||
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:cursor-pointer ${
|
||||
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out h-10 ${
|
||||
pathname === menu.url
|
||||
? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500"
|
||||
: isExpanded
|
||||
|
|
@ -315,9 +330,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
} ${level > 0 ? "ml-6" : ""}`}
|
||||
onClick={() => handleMenuClick(menu)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center min-w-0 flex-1">
|
||||
{menu.icon}
|
||||
<span className="ml-3">{menu.name}</span>
|
||||
<span className="ml-3 truncate" title={menu.name}>{menu.name}</span>
|
||||
</div>
|
||||
{menu.hasChildren && (
|
||||
<div className="ml-auto">
|
||||
|
|
@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
}`}
|
||||
onClick={() => handleMenuClick(child)}
|
||||
>
|
||||
{child.icon}
|
||||
<span className="ml-3">{child.name}</span>
|
||||
<div className="flex items-center min-w-0 flex-1">
|
||||
{child.icon}
|
||||
<span className="ml-3 truncate" title={child.name}>{child.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
{/* MainHeader 컴포넌트 사용 */}
|
||||
<MainHeader
|
||||
user={user}
|
||||
onSidebarToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
onSidebarToggle={() => {
|
||||
// 모바일에서만 토글 동작
|
||||
if (isMobile) {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
}
|
||||
}}
|
||||
onProfileClick={openProfileModal}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1">
|
||||
{/* 모바일 사이드바 오버레이 */}
|
||||
{sidebarOpen && (
|
||||
{sidebarOpen && isMobile && (
|
||||
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
|
||||
)}
|
||||
|
||||
{/* 왼쪽 사이드바 */}
|
||||
<aside
|
||||
className={`${
|
||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||
} fixed top-14 left-0 z-40 flex h-full w-64 flex-col border-r border-slate-200 bg-white transition-transform duration-300 lg:relative lg:top-0 lg:z-auto lg:h-full lg:translate-x-0 lg:transform-none`}
|
||||
isMobile
|
||||
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
|
||||
: "translate-x-0 relative top-0 z-auto"
|
||||
} flex h-full w-72 min-w-72 max-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
||||
>
|
||||
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
|
||||
{(user as ExtendedUserInfo)?.userType === "admin" && (
|
||||
|
|
@ -428,7 +452,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
</aside>
|
||||
|
||||
{/* 가운데 컨텐츠 영역 */}
|
||||
<main className="flex-1 bg-white">{children}</main>
|
||||
<main className="flex-1 min-w-0 bg-white overflow-hidden">{children}</main>
|
||||
</div>
|
||||
|
||||
{/* 프로필 수정 모달 */}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
|||
className={`mb-1 block text-sm font-medium ${hasError ? "text-destructive" : ""}`}
|
||||
style={{
|
||||
fontSize: labelStyle.labelFontSize || "14px",
|
||||
color: hasError ? "#ef4444" : labelStyle.labelColor || "#374151",
|
||||
color: hasError ? "#ef4444" : labelStyle.labelColor || "#3b83f6",
|
||||
fontWeight: labelStyle.labelFontWeight || "500",
|
||||
fontFamily: labelStyle.labelFontFamily,
|
||||
textAlign: labelStyle.labelTextAlign || "left",
|
||||
|
|
|
|||
|
|
@ -1708,7 +1708,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
|||
// 라벨 스타일 적용
|
||||
const labelStyle = {
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
padding: component.style?.labelPadding || "0",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface RealtimePreviewProps {
|
|||
selectedScreen?: any;
|
||||
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
|
||||
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
|
||||
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
|
||||
}
|
||||
|
||||
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
|
||||
|
|
@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
selectedScreen,
|
||||
onZoneComponentDrop,
|
||||
onZoneClick,
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { id, type, position, size, style: componentStyle } = component;
|
||||
|
||||
|
|
@ -89,8 +91,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
const baseStyle = {
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: `${size?.width || 100}px`,
|
||||
height: `${size?.height || 36}px`,
|
||||
width: component.componentConfig?.type === "table-list"
|
||||
? `${Math.max(size?.width || 400, 400)}px` // table-list는 최소 400px
|
||||
: `${size?.width || 100}px`,
|
||||
height: component.componentConfig?.type === "table-list"
|
||||
? `${Math.max(size?.height || 300, 300)}px` // table-list는 최소 300px
|
||||
: `${size?.height || 36}px`,
|
||||
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
||||
...componentStyle,
|
||||
};
|
||||
|
|
@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{/* 동적 컴포넌트 렌더링 */}
|
||||
<div className="h-full w-full">
|
||||
<div className={`h-full w-full ${
|
||||
component.componentConfig?.type === "table-list" ? "overflow-visible" : ""
|
||||
}`}>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isSelected={isSelected}
|
||||
|
|
@ -133,6 +141,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
selectedScreen={selectedScreen}
|
||||
onZoneComponentDrop={onZoneComponentDrop}
|
||||
onZoneClick={onZoneClick}
|
||||
onConfigChange={onConfigChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -664,6 +664,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || col.dbType,
|
||||
webType: col.webType || col.web_type,
|
||||
input_type: col.inputType || col.input_type, // 🎯 input_type 필드 추가
|
||||
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
|
||||
isNullable: col.isNullable || col.is_nullable,
|
||||
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
||||
|
|
@ -1003,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1082,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1133,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1184,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1273,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1563,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "4px",
|
||||
},
|
||||
|
|
@ -1652,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
},
|
||||
|
|
@ -1843,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
|
||||
labelFontSize: "12px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "6px",
|
||||
},
|
||||
|
|
@ -1886,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시
|
||||
labelFontSize: "12px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "6px",
|
||||
},
|
||||
|
|
@ -3157,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
{/* 실제 작업 캔버스 (해상도 크기) */}
|
||||
<div
|
||||
className="mx-auto bg-white shadow-lg"
|
||||
style={{ width: screenResolution.width, height: screenResolution.height }}
|
||||
style={{
|
||||
width: screenResolution.width,
|
||||
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
|
||||
minHeight: screenResolution.height
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="relative h-full w-full overflow-hidden bg-white"
|
||||
className="relative h-full w-full overflow-visible bg-white" // overflow-visible로 변경
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !selectionDrag.wasSelecting) {
|
||||
setSelectedComponent(null);
|
||||
|
|
@ -3270,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
selectedScreen={selectedScreen}
|
||||
// onZoneComponentDrop 제거
|
||||
onZoneClick={handleZoneClick}
|
||||
// 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
|
||||
onConfigChange={(config) => {
|
||||
console.log("📤 테이블 설정 변경을 상세설정에 알림:", config);
|
||||
// 여기서 DetailSettingsPanel의 상태를 업데이트하거나
|
||||
// 컴포넌트의 componentConfig를 업데이트할 수 있습니다
|
||||
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
|
||||
}}
|
||||
>
|
||||
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
|
||||
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
||||
|
|
@ -3350,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
selectedScreen={selectedScreen}
|
||||
// onZoneComponentDrop 제거
|
||||
onZoneClick={handleZoneClick}
|
||||
// 설정 변경 핸들러 (자식 컴포넌트용)
|
||||
onConfigChange={(config) => {
|
||||
console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
|
||||
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<Select
|
||||
value={config.action?.type || "save"}
|
||||
defaultValue="save"
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action", { type: value })}
|
||||
onValueChange={(value) => {
|
||||
// 액션 설정 업데이트
|
||||
onUpdateProperty("componentConfig.action", { type: value });
|
||||
|
||||
// 액션에 따른 라벨 색상 자동 설정
|
||||
if (value === 'delete') {
|
||||
// 삭제 액션일 때 빨간색으로 설정
|
||||
onUpdateProperty("style", {
|
||||
...component.style,
|
||||
labelColor: '#ef4444'
|
||||
});
|
||||
} else {
|
||||
// 다른 액션일 때 기본 파란색으로 리셋
|
||||
onUpdateProperty("style", {
|
||||
...component.style,
|
||||
labelColor: '#3b83f6'
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="버튼 액션 선택" />
|
||||
|
|
|
|||
|
|
@ -240,12 +240,6 @@ export const AdvancedSearchFilters: React.FC<AdvancedSearchFiltersProps> = ({
|
|||
);
|
||||
|
||||
case "code":
|
||||
console.log("🔍 코드 필터 렌더링:", {
|
||||
columnName: filter.columnName,
|
||||
codeCategory: filter.codeCategory,
|
||||
options: codeOptions[filter.codeCategory || ""],
|
||||
loading: loadingStates[filter.codeCategory || ""],
|
||||
});
|
||||
return (
|
||||
<CodeFilter
|
||||
key={filter.columnName}
|
||||
|
|
|
|||
|
|
@ -1006,13 +1006,20 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<DynamicComponentConfigPanel
|
||||
componentId={componentId}
|
||||
config={selectedComponent.componentConfig || {}}
|
||||
config={(() => {
|
||||
const config = selectedComponent.componentConfig || {};
|
||||
console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
|
||||
console.log("🔍 selectedComponent 전체:", selectedComponent);
|
||||
return config;
|
||||
})()}
|
||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||
tableColumns={(() => {
|
||||
console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
|
||||
currentTable,
|
||||
columns: currentTable?.columns,
|
||||
columnsLength: currentTable?.columns?.length,
|
||||
sampleColumn: currentTable?.columns?.[0],
|
||||
deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
|
||||
});
|
||||
return currentTable?.columns || [];
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
: "1"),
|
||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
|
||||
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
|
||||
required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false,
|
||||
readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false,
|
||||
|
|
@ -261,7 +261,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
: "1"),
|
||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
|
||||
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
|
||||
required: widget?.required || false,
|
||||
readonly: widget?.readonly || false,
|
||||
|
|
@ -285,6 +285,84 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
dragState?.justFinishedDrag, // 드래그 완료 직후 감지
|
||||
]);
|
||||
|
||||
// 🔴 삭제 액션일 때 라벨 색상 자동 설정
|
||||
useEffect(() => {
|
||||
if (selectedComponent && selectedComponent.type === "component") {
|
||||
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
||||
const isDeleteAction = () => {
|
||||
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del'];
|
||||
return (
|
||||
selectedComponent.componentConfig?.action?.type === 'delete' ||
|
||||
selectedComponent.config?.action?.type === 'delete' ||
|
||||
selectedComponent.webTypeConfig?.actionType === 'delete' ||
|
||||
selectedComponent.text?.toLowerCase().includes('삭제') ||
|
||||
selectedComponent.text?.toLowerCase().includes('delete') ||
|
||||
selectedComponent.label?.toLowerCase().includes('삭제') ||
|
||||
selectedComponent.label?.toLowerCase().includes('delete') ||
|
||||
deleteKeywords.some(keyword =>
|
||||
selectedComponent.config?.buttonText?.toLowerCase().includes(keyword) ||
|
||||
selectedComponent.config?.text?.toLowerCase().includes(keyword)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 🔍 디버깅: 컴포넌트 구조 확인
|
||||
console.log("🔍 PropertiesPanel 삭제 액션 디버깅:", {
|
||||
componentType: selectedComponent.type,
|
||||
componentId: selectedComponent.id,
|
||||
componentConfig: selectedComponent.componentConfig,
|
||||
config: selectedComponent.config,
|
||||
webTypeConfig: selectedComponent.webTypeConfig,
|
||||
actionType1: selectedComponent.componentConfig?.action?.type,
|
||||
actionType2: selectedComponent.config?.action?.type,
|
||||
actionType3: selectedComponent.webTypeConfig?.actionType,
|
||||
isDeleteAction: isDeleteAction(),
|
||||
currentLabelColor: selectedComponent.style?.labelColor,
|
||||
});
|
||||
|
||||
// 액션에 따른 라벨 색상 자동 설정
|
||||
if (isDeleteAction()) {
|
||||
// 삭제 액션일 때 빨간색으로 설정 (이미 빨간색이 아닌 경우에만)
|
||||
if (selectedComponent.style?.labelColor !== '#ef4444') {
|
||||
console.log("🔴 삭제 액션 감지: 라벨 색상을 빨간색으로 자동 설정");
|
||||
onUpdateProperty("style", {
|
||||
...selectedComponent.style,
|
||||
labelColor: '#ef4444'
|
||||
});
|
||||
|
||||
// 로컬 입력 상태도 업데이트
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
labelColor: '#ef4444'
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// 다른 액션일 때 기본 파란색으로 리셋 (현재 빨간색인 경우에만)
|
||||
if (selectedComponent.style?.labelColor === '#ef4444') {
|
||||
console.log("🔵 일반 액션 감지: 라벨 색상을 기본 파란색으로 리셋");
|
||||
onUpdateProperty("style", {
|
||||
...selectedComponent.style,
|
||||
labelColor: '#3b83f6'
|
||||
});
|
||||
|
||||
// 로컬 입력 상태도 업데이트
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
labelColor: '#3b83f6'
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
selectedComponent?.componentConfig?.action?.type,
|
||||
selectedComponent?.config?.action?.type,
|
||||
selectedComponent?.webTypeConfig?.actionType,
|
||||
selectedComponent?.id,
|
||||
selectedComponent?.style?.labelColor, // 라벨 색상 변경도 감지
|
||||
JSON.stringify(selectedComponent?.componentConfig), // 전체 componentConfig 변경 감지
|
||||
onUpdateProperty
|
||||
]);
|
||||
|
||||
// 렌더링 시마다 실행되는 직접적인 드래그 상태 체크
|
||||
if (dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id) {
|
||||
console.log("🎯 렌더링 중 드래그 상태 감지:", {
|
||||
|
|
|
|||
|
|
@ -18,40 +18,36 @@ interface EntityTypeConfigPanelProps {
|
|||
export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ config, onConfigChange }) => {
|
||||
// 기본값이 설정된 config 사용
|
||||
const safeConfig = {
|
||||
entityName: "",
|
||||
displayField: "name",
|
||||
valueField: "id",
|
||||
searchable: true,
|
||||
multiple: false,
|
||||
allowClear: true,
|
||||
referenceTable: "",
|
||||
referenceColumn: "id",
|
||||
displayColumns: config.displayColumns || (config.displayColumn ? [config.displayColumn] : ["name"]), // 호환성 처리
|
||||
searchColumns: [],
|
||||
filters: {},
|
||||
placeholder: "",
|
||||
apiEndpoint: "",
|
||||
filters: [],
|
||||
displayFormat: "simple",
|
||||
maxSelections: undefined,
|
||||
separator: " - ",
|
||||
...config,
|
||||
};
|
||||
|
||||
// 로컬 상태로 실시간 입력 관리
|
||||
const [localValues, setLocalValues] = useState({
|
||||
entityName: safeConfig.entityName,
|
||||
displayField: safeConfig.displayField,
|
||||
valueField: safeConfig.valueField,
|
||||
searchable: safeConfig.searchable,
|
||||
multiple: safeConfig.multiple,
|
||||
allowClear: safeConfig.allowClear,
|
||||
referenceTable: safeConfig.referenceTable,
|
||||
referenceColumn: safeConfig.referenceColumn,
|
||||
displayColumns: [...safeConfig.displayColumns],
|
||||
searchColumns: [...(safeConfig.searchColumns || [])],
|
||||
placeholder: safeConfig.placeholder,
|
||||
apiEndpoint: safeConfig.apiEndpoint,
|
||||
displayFormat: safeConfig.displayFormat,
|
||||
maxSelections: safeConfig.maxSelections?.toString() || "",
|
||||
separator: safeConfig.separator,
|
||||
});
|
||||
|
||||
const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" });
|
||||
const [newDisplayColumn, setNewDisplayColumn] = useState("");
|
||||
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
|
||||
|
||||
// 표시 형식 옵션
|
||||
const displayFormats = [
|
||||
{ value: "simple", label: "단순 (이름만)" },
|
||||
{ value: "detailed", label: "상세 (이름 + 설명)" },
|
||||
{ value: "simple", label: "단순 (첫 번째 컬럼만)" },
|
||||
{ value: "detailed", label: "상세 (모든 컬럼 표시)" },
|
||||
{ value: "custom", label: "사용자 정의" },
|
||||
];
|
||||
|
||||
|
|
@ -71,37 +67,27 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
// config가 변경될 때 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
setLocalValues({
|
||||
entityName: safeConfig.entityName,
|
||||
displayField: safeConfig.displayField,
|
||||
valueField: safeConfig.valueField,
|
||||
searchable: safeConfig.searchable,
|
||||
multiple: safeConfig.multiple,
|
||||
allowClear: safeConfig.allowClear,
|
||||
referenceTable: safeConfig.referenceTable,
|
||||
referenceColumn: safeConfig.referenceColumn,
|
||||
displayColumns: [...safeConfig.displayColumns],
|
||||
searchColumns: [...(safeConfig.searchColumns || [])],
|
||||
placeholder: safeConfig.placeholder,
|
||||
apiEndpoint: safeConfig.apiEndpoint,
|
||||
displayFormat: safeConfig.displayFormat,
|
||||
maxSelections: safeConfig.maxSelections?.toString() || "",
|
||||
separator: safeConfig.separator,
|
||||
});
|
||||
}, [
|
||||
safeConfig.entityName,
|
||||
safeConfig.displayField,
|
||||
safeConfig.valueField,
|
||||
safeConfig.searchable,
|
||||
safeConfig.multiple,
|
||||
safeConfig.allowClear,
|
||||
safeConfig.referenceTable,
|
||||
safeConfig.referenceColumn,
|
||||
safeConfig.displayColumns,
|
||||
safeConfig.searchColumns,
|
||||
safeConfig.placeholder,
|
||||
safeConfig.apiEndpoint,
|
||||
safeConfig.displayFormat,
|
||||
safeConfig.maxSelections,
|
||||
safeConfig.separator,
|
||||
]);
|
||||
|
||||
const updateConfig = (key: keyof EntityTypeConfig, value: any) => {
|
||||
// 로컬 상태 즉시 업데이트
|
||||
if (key === "maxSelections") {
|
||||
setLocalValues((prev) => ({ ...prev, [key]: value?.toString() || "" }));
|
||||
} else {
|
||||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// 실제 config 업데이트
|
||||
const newConfig = { ...safeConfig, [key]: value };
|
||||
|
|
@ -114,82 +100,131 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
onConfigChange(newConfig);
|
||||
};
|
||||
|
||||
// 표시 컬럼 추가
|
||||
const addDisplayColumn = () => {
|
||||
if (newDisplayColumn.trim() && !localValues.displayColumns.includes(newDisplayColumn.trim())) {
|
||||
const updatedColumns = [...localValues.displayColumns, newDisplayColumn.trim()];
|
||||
updateConfig("displayColumns", updatedColumns);
|
||||
setNewDisplayColumn("");
|
||||
}
|
||||
};
|
||||
|
||||
// 표시 컬럼 제거
|
||||
const removeDisplayColumn = (index: number) => {
|
||||
const updatedColumns = localValues.displayColumns.filter((_, i) => i !== index);
|
||||
updateConfig("displayColumns", updatedColumns);
|
||||
};
|
||||
|
||||
const addFilter = () => {
|
||||
if (newFilter.field.trim() && newFilter.value.trim()) {
|
||||
const updatedFilters = [...(safeConfig.filters || []), { ...newFilter }];
|
||||
const updatedFilters = { ...safeConfig.filters, [newFilter.field]: newFilter.value };
|
||||
updateConfig("filters", updatedFilters);
|
||||
setNewFilter({ field: "", operator: "=", value: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const removeFilter = (index: number) => {
|
||||
const updatedFilters = (safeConfig.filters || []).filter((_, i) => i !== index);
|
||||
const removeFilter = (field: string) => {
|
||||
const updatedFilters = { ...safeConfig.filters };
|
||||
delete updatedFilters[field];
|
||||
updateConfig("filters", updatedFilters);
|
||||
};
|
||||
|
||||
const updateFilter = (index: number, field: keyof typeof newFilter, value: string) => {
|
||||
const updatedFilters = [...(safeConfig.filters || [])];
|
||||
updatedFilters[index] = { ...updatedFilters[index], [field]: value };
|
||||
const updateFilter = (oldField: string, field: string, value: string) => {
|
||||
const updatedFilters = { ...safeConfig.filters };
|
||||
if (oldField !== field) {
|
||||
delete updatedFilters[oldField];
|
||||
}
|
||||
updatedFilters[field] = value;
|
||||
updateConfig("filters", updatedFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 엔터티 이름 */}
|
||||
{/* 참조 테이블 */}
|
||||
<div>
|
||||
<Label htmlFor="entityName" className="text-sm font-medium">
|
||||
엔터티 이름
|
||||
<Label htmlFor="referenceTable" className="text-sm font-medium">
|
||||
참조 테이블
|
||||
</Label>
|
||||
<Input
|
||||
id="entityName"
|
||||
value={localValues.entityName}
|
||||
onChange={(e) => updateConfig("entityName", e.target.value)}
|
||||
placeholder="예: User, Company, Product"
|
||||
id="referenceTable"
|
||||
value={localValues.referenceTable}
|
||||
onChange={(e) => updateConfig("referenceTable", e.target.value)}
|
||||
placeholder="예: user_info, company_info"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API 엔드포인트 */}
|
||||
{/* 조인 컬럼 (값 필드) */}
|
||||
<div>
|
||||
<Label htmlFor="apiEndpoint" className="text-sm font-medium">
|
||||
API 엔드포인트
|
||||
<Label htmlFor="referenceColumn" className="text-sm font-medium">
|
||||
조인 컬럼 (값 필드)
|
||||
</Label>
|
||||
<Input
|
||||
id="apiEndpoint"
|
||||
value={localValues.apiEndpoint}
|
||||
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
|
||||
placeholder="예: /api/users"
|
||||
id="referenceColumn"
|
||||
value={localValues.referenceColumn}
|
||||
onChange={(e) => updateConfig("referenceColumn", e.target.value)}
|
||||
placeholder="id, user_id, company_code"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 필드 설정 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="valueField" className="text-sm font-medium">
|
||||
값 필드
|
||||
</Label>
|
||||
<Input
|
||||
id="valueField"
|
||||
value={localValues.valueField}
|
||||
onChange={(e) => updateConfig("valueField", e.target.value)}
|
||||
placeholder="id"
|
||||
className="mt-1"
|
||||
/>
|
||||
{/* 표시 컬럼들 (다중 선택) */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">표시 컬럼들</Label>
|
||||
|
||||
{/* 현재 선택된 표시 컬럼들 */}
|
||||
<div className="space-y-2">
|
||||
{localValues.displayColumns.map((column, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 rounded border bg-gray-50 p-2">
|
||||
<Database className="h-4 w-4 text-gray-500" />
|
||||
<span className="flex-1 text-sm font-medium">{column}</span>
|
||||
<Button size="sm" variant="outline" onClick={() => removeDisplayColumn(index)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{localValues.displayColumns.length === 0 && (
|
||||
<div className="text-sm text-gray-500 italic">표시할 컬럼을 추가해주세요</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="displayField" className="text-sm font-medium">
|
||||
표시 필드
|
||||
</Label>
|
||||
{/* 새 표시 컬럼 추가 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
id="displayField"
|
||||
value={localValues.displayField}
|
||||
onChange={(e) => updateConfig("displayField", e.target.value)}
|
||||
placeholder="name"
|
||||
className="mt-1"
|
||||
value={newDisplayColumn}
|
||||
onChange={(e) => setNewDisplayColumn(e.target.value)}
|
||||
placeholder="컬럼명 입력 (예: user_name, dept_name)"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={addDisplayColumn}
|
||||
disabled={!newDisplayColumn.trim() || localValues.displayColumns.includes(newDisplayColumn.trim())}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
• 여러 컬럼을 선택하면 "{localValues.separator || " - "}"로 구분하여 표시됩니다
|
||||
<br />• 예: 이름{localValues.separator || " - "}부서명
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분자 설정 */}
|
||||
<div>
|
||||
<Label htmlFor="separator" className="text-sm font-medium">
|
||||
구분자
|
||||
</Label>
|
||||
<Input
|
||||
id="separator"
|
||||
value={localValues.separator}
|
||||
onChange={(e) => updateConfig("separator", e.target.value)}
|
||||
placeholder=" - "
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 표시 형식 */}
|
||||
|
|
@ -225,93 +260,28 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 옵션들 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="searchable" className="text-sm font-medium">
|
||||
검색 가능
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="searchable"
|
||||
checked={localValues.searchable}
|
||||
onCheckedChange={(checked) => updateConfig("searchable", !!checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="multiple" className="text-sm font-medium">
|
||||
다중 선택
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="multiple"
|
||||
checked={localValues.multiple}
|
||||
onCheckedChange={(checked) => updateConfig("multiple", !!checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="allowClear" className="text-sm font-medium">
|
||||
선택 해제 허용
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="allowClear"
|
||||
checked={localValues.allowClear}
|
||||
onCheckedChange={(checked) => updateConfig("allowClear", !!checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 최대 선택 개수 (다중 선택 시) */}
|
||||
{localValues.multiple && (
|
||||
<div>
|
||||
<Label htmlFor="maxSelections" className="text-sm font-medium">
|
||||
최대 선택 개수
|
||||
</Label>
|
||||
<Input
|
||||
id="maxSelections"
|
||||
type="number"
|
||||
min="1"
|
||||
value={localValues.maxSelections}
|
||||
onChange={(e) => updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
placeholder="제한 없음"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필터 관리 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">데이터 필터</Label>
|
||||
|
||||
{/* 기존 필터 목록 */}
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{(safeConfig.filters || []).map((filter, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 rounded border p-2 text-sm">
|
||||
{Object.entries(safeConfig.filters || {}).map(([field, value]) => (
|
||||
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-sm">
|
||||
<Input
|
||||
value={filter.field}
|
||||
onChange={(e) => updateFilter(index, "field", e.target.value)}
|
||||
value={field}
|
||||
onChange={(e) => updateFilter(field, e.target.value, value as string)}
|
||||
placeholder="필드명"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select value={filter.operator} onValueChange={(value) => updateFilter(index, "operator", value)}>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-gray-500">=</span>
|
||||
<Input
|
||||
value={filter.value}
|
||||
onChange={(e) => updateFilter(index, "value", e.target.value)}
|
||||
value={value as string}
|
||||
onChange={(e) => updateFilter(field, field, e.target.value)}
|
||||
placeholder="값"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button size="sm" variant="outline" onClick={() => removeFilter(index)}>
|
||||
<Button size="sm" variant="outline" onClick={() => removeFilter(field)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -326,21 +296,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
placeholder="필드명"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select
|
||||
value={newFilter.operator}
|
||||
onValueChange={(value) => setNewFilter((prev) => ({ ...prev, operator: value }))}
|
||||
>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-gray-500">=</span>
|
||||
<Input
|
||||
value={newFilter.value}
|
||||
onChange={(e) => setNewFilter((prev) => ({ ...prev, value: e.target.value }))}
|
||||
|
|
@ -352,7 +308,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">총 {(safeConfig.filters || []).length}개 필터</div>
|
||||
<div className="text-xs text-gray-500">총 {Object.keys(safeConfig.filters || {}).length}개 필터</div>
|
||||
</div>
|
||||
|
||||
{/* 미리보기 */}
|
||||
|
|
@ -360,31 +316,33 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
|||
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center space-x-2 rounded border bg-white p-2">
|
||||
{localValues.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
||||
<Search className="h-4 w-4 text-gray-400" />
|
||||
<div className="flex-1 text-sm text-gray-600">
|
||||
{localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`}
|
||||
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
|
||||
</div>
|
||||
<Database className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
엔터티: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, 값필드:{" "}
|
||||
{localValues.valueField}, 표시필드: {localValues.displayField}
|
||||
{localValues.multiple && `, 다중선택`}
|
||||
{localValues.searchable && `, 검색가능`}
|
||||
참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn}
|
||||
<br />
|
||||
표시컬럼:{" "}
|
||||
{localValues.displayColumns.length > 0
|
||||
? localValues.displayColumns.join(localValues.separator || " - ")
|
||||
: "없음"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 안내 메시지 */}
|
||||
<div className="rounded-md bg-blue-50 p-3">
|
||||
<div className="text-sm font-medium text-blue-900">엔터티 참조 설정</div>
|
||||
<div className="text-sm font-medium text-blue-900">엔터티 타입 설정 가이드</div>
|
||||
<div className="mt-1 text-xs text-blue-800">
|
||||
• 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다
|
||||
• <strong>참조 테이블</strong>: 데이터를 가져올 다른 테이블 이름
|
||||
<br />• <strong>조인 컬럼</strong>: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID)
|
||||
<br />• <strong>표시 컬럼</strong>: 사용자에게 보여질 컬럼들 (여러 개 가능)
|
||||
<br />
|
||||
• API 엔드포인트를 통해 데이터를 동적으로 로드합니다
|
||||
<br />
|
||||
• 필터를 사용하여 표시할 데이터를 제한할 수 있습니다
|
||||
<br />• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다
|
||||
• 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다
|
||||
<br />• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export const entityJoinApi = {
|
|||
sourceColumn: string;
|
||||
joinAlias: string;
|
||||
}>;
|
||||
screenEntityConfigs?: Record<string, any>; // 🎯 화면별 엔티티 설정
|
||||
} = {},
|
||||
): Promise<EntityJoinResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
|
@ -93,6 +94,7 @@ export const entityJoinApi = {
|
|||
...params,
|
||||
search: params.search ? JSON.stringify(params.search) : undefined,
|
||||
additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined,
|
||||
screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
const totalRequests = useRef(0);
|
||||
const cacheHits = useRef(0);
|
||||
const batchLoadCount = useRef(0);
|
||||
|
||||
// 변환된 값 캐시 (중복 변환 방지)
|
||||
const convertedCache = useRef(new Map<string, string>());
|
||||
|
||||
// 공통 코드 카테고리 추출 (메모이제이션)
|
||||
const codeCategories = useMemo(() => {
|
||||
|
|
@ -175,29 +178,41 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
const startTime = Date.now();
|
||||
totalRequests.current += 1;
|
||||
|
||||
// 🎯 디버깅: 캐시 상태 로깅
|
||||
console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`);
|
||||
// 🎯 중복 호출 방지: 이미 변환된 값인지 확인
|
||||
const cacheKey = `${categoryCode}:${codeValue}`;
|
||||
if (convertedCache.current.has(cacheKey)) {
|
||||
return convertedCache.current.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// 🎯 디버깅: 캐시 상태 로깅 (빈도 줄이기)
|
||||
if (totalRequests.current % 10 === 1) { // 10번마다 한 번만 로깅
|
||||
console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`);
|
||||
}
|
||||
|
||||
// 캐시에서 동기적으로 조회 시도
|
||||
const syncResult = codeCache.getCodeSync(categoryCode);
|
||||
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
|
||||
}
|
||||
|
||||
// 🎯 캐시 내용 상세 로깅 (키값들 확인)
|
||||
if (syncResult) {
|
||||
// 🎯 캐시 내용 상세 로깅 (키값들 확인) - 빈도 줄이기
|
||||
if (syncResult && totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 캐시 키값들:`, Object.keys(syncResult));
|
||||
console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2));
|
||||
}
|
||||
|
||||
if (syncResult && Array.isArray(syncResult)) {
|
||||
cacheHits.current += 1;
|
||||
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
|
||||
console.log(
|
||||
`🔍 캐시 배열 내용:`,
|
||||
syncResult.map((item) => ({
|
||||
code_value: item.code_value,
|
||||
code_name: item.code_name,
|
||||
})),
|
||||
);
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
|
||||
console.log(
|
||||
`🔍 캐시 배열 내용:`,
|
||||
syncResult.map((item) => ({
|
||||
code_value: item.code_value,
|
||||
code_name: item.code_name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// 배열에서 해당 code_value를 가진 항목 찾기
|
||||
const foundCode = syncResult.find(
|
||||
|
|
@ -205,7 +220,13 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
);
|
||||
|
||||
const result = foundCode ? foundCode.code_name : codeValue;
|
||||
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
|
||||
|
||||
// 변환 결과를 캐시에 저장
|
||||
convertedCache.current.set(cacheKey, result);
|
||||
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
|
||||
}
|
||||
|
||||
// 응답 시간 추적 (캐시 히트)
|
||||
requestTimes.current.push(Date.now() - startTime);
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export class AutoRegisteringComponentRenderer {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
marginBottom: component.style?.labelMarginBottom || "4px",
|
||||
fontWeight: "500",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export interface ComponentRenderer {
|
|||
refreshKey?: number;
|
||||
// 편집 모드
|
||||
mode?: "view" | "edit";
|
||||
// 설정 변경 핸들러 (상세설정과 연동)
|
||||
onConfigChange?: (config: any) => void;
|
||||
[key: string]: any;
|
||||
}): React.ReactElement;
|
||||
}
|
||||
|
|
@ -170,6 +172,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
selectedRowsData,
|
||||
onSelectedRowsChange,
|
||||
refreshKey,
|
||||
onConfigChange,
|
||||
...safeProps
|
||||
} = props;
|
||||
|
||||
|
|
@ -224,6 +227,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
selectedRows={selectedRows}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
// 설정 변경 핸들러 전달
|
||||
onConfigChange={onConfigChange}
|
||||
refreshKey={refreshKey}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ export const AccordionBasicComponent: React.FC<AccordionBasicComponentProps> = (
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -86,12 +86,69 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
||||
const isDeleteAction = () => {
|
||||
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del'];
|
||||
return (
|
||||
component.componentConfig?.action?.type === 'delete' ||
|
||||
component.config?.action?.type === 'delete' ||
|
||||
component.webTypeConfig?.actionType === 'delete' ||
|
||||
component.text?.toLowerCase().includes('삭제') ||
|
||||
component.text?.toLowerCase().includes('delete') ||
|
||||
component.label?.toLowerCase().includes('삭제') ||
|
||||
component.label?.toLowerCase().includes('delete') ||
|
||||
deleteKeywords.some(keyword =>
|
||||
component.config?.buttonText?.toLowerCase().includes(keyword) ||
|
||||
component.config?.text?.toLowerCase().includes(keyword)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 삭제 액션일 때 라벨 색상 자동 설정
|
||||
useEffect(() => {
|
||||
if (isDeleteAction() && !component.style?.labelColor) {
|
||||
// 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정
|
||||
if (component.style) {
|
||||
component.style.labelColor = '#ef4444';
|
||||
} else {
|
||||
component.style = { labelColor: '#ef4444' };
|
||||
}
|
||||
}
|
||||
}, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]);
|
||||
|
||||
// 컴포넌트 설정
|
||||
const componentConfig = {
|
||||
...config,
|
||||
...component.config,
|
||||
} as ButtonPrimaryConfig;
|
||||
|
||||
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
|
||||
const getLabelColor = () => {
|
||||
if (isDeleteAction()) {
|
||||
return component.style?.labelColor || '#ef4444'; // 빨간색 기본값 (Tailwind red-500)
|
||||
}
|
||||
return component.style?.labelColor || '#3b83f6'; // 기본 파란색 (Tailwind blue-500)
|
||||
};
|
||||
|
||||
const buttonColor = getLabelColor();
|
||||
|
||||
// 그라데이션용 어두운 색상 계산
|
||||
const getDarkColor = (baseColor: string) => {
|
||||
const hex = baseColor.replace('#', '');
|
||||
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40);
|
||||
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40);
|
||||
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40);
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const buttonDarkColor = getDarkColor(buttonColor);
|
||||
|
||||
console.log("🎨 동적 색상 연동:", {
|
||||
labelColor: component.style?.labelColor,
|
||||
buttonColor,
|
||||
buttonDarkColor,
|
||||
});
|
||||
|
||||
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
||||
const processedConfig = { ...componentConfig };
|
||||
if (componentConfig.action && typeof componentConfig.action === "string") {
|
||||
|
|
@ -368,26 +425,29 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: "100%", // 최소 높이 강제 적용
|
||||
maxHeight: "100%", // 최대 높이 제한
|
||||
border: "1px solid #3b82f6",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "white",
|
||||
minHeight: "100%",
|
||||
maxHeight: "100%",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
background: componentConfig.disabled
|
||||
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
||||
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||
color: componentConfig.disabled ? "#9ca3af" : "white",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
fontWeight: "600",
|
||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||
outline: "none",
|
||||
boxSizing: "border-box", // 패딩/보더 포함 크기 계산
|
||||
display: "flex", // flex로 변경
|
||||
alignItems: "center", // 세로 중앙 정렬
|
||||
justifyContent: "center", // 가로 중앙 정렬
|
||||
padding: "0", // 패딩 제거
|
||||
margin: "0", // 마진 제거
|
||||
lineHeight: "1", // 라인 높이 고정
|
||||
// 강제 높이 적용
|
||||
boxSizing: "border-box",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 16px",
|
||||
margin: "0",
|
||||
lineHeight: "1",
|
||||
minHeight: "36px",
|
||||
height: "36px",
|
||||
boxShadow: componentConfig.disabled
|
||||
? "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
|
||||
: `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -141,7 +141,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
|||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const ImageDisplayComponent: React.FC<ImageDisplayComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -155,7 +155,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
|||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -149,7 +149,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
|||
width: "30%",
|
||||
textAlign: "center",
|
||||
fontSize: "14px",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface SingleTableWithStickyProps {
|
|||
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
|
||||
formatCellValue: (value: any, format?: string, columnName?: string) => string;
|
||||
getColumnWidth: (column: ColumnConfig) => number;
|
||||
containerWidth?: string; // 컨테이너 너비 설정
|
||||
}
|
||||
|
||||
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
|
|
@ -39,12 +40,28 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
renderCheckboxCell,
|
||||
formatCellValue,
|
||||
getColumnWidth,
|
||||
containerWidth,
|
||||
}) => {
|
||||
const checkboxConfig = tableConfig.checkbox || {};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<Table className="w-full">
|
||||
<div
|
||||
className="relative h-full overflow-auto"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
tableLayout: "fixed",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<TableRow>
|
||||
{visibleColumns.map((column, colIndex) => {
|
||||
|
|
@ -81,6 +98,9 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
width: getColumnWidth(column),
|
||||
minWidth: getColumnWidth(column),
|
||||
maxWidth: getColumnWidth(column),
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
|
|
@ -90,7 +110,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
<div className="flex items-center gap-2">
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
checkboxConfig.selectAll && (
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" style={{ zIndex: 1 }} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -167,6 +187,11 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
minHeight: "40px",
|
||||
height: "40px",
|
||||
verticalAlign: "middle",
|
||||
width: getColumnWidth(column),
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,6 +12,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TableListConfig, ColumnConfig } from "./types";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
|
||||
|
||||
export interface TableListConfigPanelProps {
|
||||
|
|
@ -31,7 +32,18 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
screenTableName,
|
||||
tableColumns,
|
||||
}) => {
|
||||
console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns });
|
||||
console.log("🔍 TableListConfigPanel props:", {
|
||||
config,
|
||||
configType: typeof config,
|
||||
configSelectedTable: config?.selectedTable,
|
||||
configPagination: config?.pagination,
|
||||
paginationEnabled: config?.pagination?.enabled,
|
||||
paginationPageSize: config?.pagination?.pageSize,
|
||||
configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
|
||||
screenTableName,
|
||||
tableColumns: tableColumns?.length,
|
||||
tableColumnsSample: tableColumns?.[0],
|
||||
});
|
||||
|
||||
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||
const [loadingTables, setLoadingTables] = useState(false);
|
||||
|
|
@ -58,8 +70,22 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
}>;
|
||||
}>;
|
||||
}>({ availableColumns: [], joinTables: [] });
|
||||
|
||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||
|
||||
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
|
||||
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||||
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||||
selectedColumns: string[];
|
||||
separator: string;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
|
||||
// 화면 테이블명이 있으면 자동으로 설정
|
||||
useEffect(() => {
|
||||
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
|
||||
|
|
@ -73,18 +99,14 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
const fetchTables = async () => {
|
||||
setLoadingTables(true);
|
||||
try {
|
||||
const response = await fetch("/api/tables");
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setAvailableTables(
|
||||
result.data.map((table: any) => ({
|
||||
tableName: table.tableName,
|
||||
displayName: table.displayName || table.tableName,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
// API 클라이언트를 사용하여 올바른 포트로 호출
|
||||
const response = await tableTypeApi.getTables();
|
||||
setAvailableTables(
|
||||
response.map((table: any) => ({
|
||||
tableName: table.tableName,
|
||||
displayName: table.displayName || table.tableName,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 가져오기 실패:", error);
|
||||
} finally {
|
||||
|
|
@ -194,13 +216,25 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
|
||||
console.log("🔧 TableListConfigPanel handleNestedChange:", {
|
||||
parentKey,
|
||||
childKey,
|
||||
value,
|
||||
parentValue: config[parentKey],
|
||||
hasOnChange: !!onChange,
|
||||
onChangeType: typeof onChange,
|
||||
});
|
||||
|
||||
const parentValue = config[parentKey] as any;
|
||||
onChange({
|
||||
const newConfig = {
|
||||
[parentKey]: {
|
||||
...parentValue,
|
||||
[childKey]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
|
||||
onChange(newConfig);
|
||||
};
|
||||
|
||||
// 컬럼 추가
|
||||
|
|
@ -228,30 +262,26 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
handleChange("columns", [...(config.columns || []), newColumn]);
|
||||
};
|
||||
|
||||
// Entity 조인 컬럼 추가
|
||||
const addEntityJoinColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
|
||||
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
|
||||
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
|
||||
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
|
||||
if (existingColumn) return;
|
||||
|
||||
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
|
||||
const newColumn: ColumnConfig = {
|
||||
columnName: joinColumn.joinAlias,
|
||||
displayName: joinColumn.columnLabel, // 라벨명만 사용
|
||||
displayName: joinColumn.columnLabel,
|
||||
visible: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
align: "left",
|
||||
format: "text",
|
||||
order: config.columns?.length || 0,
|
||||
isEntityJoin: true, // Entity 조인 컬럼임을 표시
|
||||
entityJoinInfo: {
|
||||
sourceTable: joinColumn.tableName,
|
||||
sourceColumn: joinColumn.columnName,
|
||||
joinAlias: joinColumn.joinAlias,
|
||||
},
|
||||
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
|
||||
};
|
||||
|
||||
handleChange("columns", [...(config.columns || []), newColumn]);
|
||||
console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn);
|
||||
console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn);
|
||||
};
|
||||
|
||||
// 컬럼 제거
|
||||
|
|
@ -267,6 +297,333 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
handleChange("columns", updatedColumns);
|
||||
};
|
||||
|
||||
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
|
||||
useEffect(() => {
|
||||
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
|
||||
hasColumns: !!config.columns,
|
||||
columnsCount: config.columns?.length || 0,
|
||||
hasTableColumns: !!tableColumns,
|
||||
tableColumnsCount: tableColumns?.length || 0,
|
||||
selectedTable: config.selectedTable,
|
||||
});
|
||||
|
||||
if (!config.columns || !tableColumns) {
|
||||
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedColumns = config.columns.map((column) => {
|
||||
// 이미 isEntityJoin이 설정된 경우 스킵
|
||||
if (column.isEntityJoin) {
|
||||
console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName);
|
||||
return column;
|
||||
}
|
||||
|
||||
// 테이블 컬럼 정보에서 해당 컬럼 찾기
|
||||
const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName);
|
||||
console.log("🔍 컬럼 검색:", {
|
||||
columnName: column.columnName,
|
||||
found: !!tableColumn,
|
||||
inputType: tableColumn?.input_type,
|
||||
webType: tableColumn?.web_type,
|
||||
});
|
||||
|
||||
// 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인)
|
||||
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
|
||||
console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", {
|
||||
columnName: column.columnName,
|
||||
referenceTable: tableColumn.reference_table,
|
||||
referenceTableAlt: tableColumn.referenceTable,
|
||||
allTableColumnKeys: Object.keys(tableColumn),
|
||||
});
|
||||
|
||||
return {
|
||||
...column,
|
||||
isEntityJoin: true,
|
||||
entityJoinInfo: {
|
||||
sourceTable: config.selectedTable || "",
|
||||
sourceColumn: column.columnName,
|
||||
joinAlias: column.columnName,
|
||||
},
|
||||
entityDisplayConfig: {
|
||||
displayColumns: [], // 빈 배열로 초기화
|
||||
separator: " - ",
|
||||
sourceTable: config.selectedTable || "",
|
||||
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
|
||||
// 변경사항이 있는 경우에만 업데이트
|
||||
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
|
||||
|
||||
if (hasChanges) {
|
||||
console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns);
|
||||
handleChange("columns", updatedColumns);
|
||||
} else {
|
||||
console.log("ℹ️ 엔티티 컬럼 변경사항 없음");
|
||||
}
|
||||
}, [config.columns, tableColumns, config.selectedTable]);
|
||||
|
||||
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
|
||||
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
|
||||
console.log("🔍 loadEntityDisplayConfig 시작:", {
|
||||
columnName: column.columnName,
|
||||
isEntityJoin: column.isEntityJoin,
|
||||
entityJoinInfo: column.entityJoinInfo,
|
||||
entityDisplayConfig: column.entityDisplayConfig,
|
||||
configSelectedTable: config.selectedTable,
|
||||
});
|
||||
|
||||
if (!column.isEntityJoin || !column.entityJoinInfo) {
|
||||
console.log("⚠️ 엔티티 컬럼 조건 불만족:", {
|
||||
isEntityJoin: column.isEntityJoin,
|
||||
entityJoinInfo: column.entityJoinInfo,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// entityDisplayConfig가 없으면 초기화
|
||||
if (!column.entityDisplayConfig) {
|
||||
console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
|
||||
const updatedColumns = config.columns?.map((col) => {
|
||||
if (col.columnName === column.columnName) {
|
||||
return {
|
||||
...col,
|
||||
entityDisplayConfig: {
|
||||
displayColumns: [],
|
||||
separator: " - ",
|
||||
sourceTable: config.selectedTable || "",
|
||||
joinTable: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
// 업데이트된 컬럼으로 다시 시도
|
||||
const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName);
|
||||
if (updatedColumn) {
|
||||
console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig);
|
||||
return loadEntityDisplayConfig(updatedColumn);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
|
||||
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
|
||||
|
||||
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기
|
||||
let sourceTable = column.entityDisplayConfig.sourceTable;
|
||||
let joinTable = column.entityDisplayConfig.joinTable;
|
||||
|
||||
if (!sourceTable && column.entityJoinInfo) {
|
||||
sourceTable = column.entityJoinInfo.sourceTable;
|
||||
}
|
||||
|
||||
if (!joinTable) {
|
||||
// joinTable이 없으면 tableTypeApi로 조회해서 설정
|
||||
try {
|
||||
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
|
||||
const columnList = await tableTypeApi.getColumns(sourceTable);
|
||||
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
||||
|
||||
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
||||
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
||||
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable);
|
||||
|
||||
// entityDisplayConfig 업데이트
|
||||
const updatedConfig = {
|
||||
...column.entityDisplayConfig,
|
||||
sourceTable: sourceTable,
|
||||
joinTable: joinTable,
|
||||
};
|
||||
|
||||
// 컬럼 설정 업데이트
|
||||
const updatedColumns = config.columns?.map((col) =>
|
||||
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
||||
);
|
||||
|
||||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
|
||||
const configKey = `${column.columnName}`;
|
||||
|
||||
// 이미 로드된 경우 스킵
|
||||
if (entityDisplayConfigs[configKey]) return;
|
||||
|
||||
// joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기
|
||||
let actualJoinTable = joinTable;
|
||||
if (!actualJoinTable && sourceTable) {
|
||||
try {
|
||||
console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", {
|
||||
tableName: sourceTable,
|
||||
columnName: column.columnName,
|
||||
});
|
||||
|
||||
const columnList = await tableTypeApi.getColumns(sourceTable);
|
||||
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
||||
|
||||
console.log("🔍 컬럼 정보 조회 결과:", {
|
||||
columnInfo: columnInfo,
|
||||
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
|
||||
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
|
||||
});
|
||||
|
||||
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
||||
actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
||||
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable);
|
||||
|
||||
// entityDisplayConfig 업데이트
|
||||
const updatedConfig = {
|
||||
...column.entityDisplayConfig,
|
||||
joinTable: actualJoinTable,
|
||||
};
|
||||
|
||||
// 컬럼 설정 업데이트
|
||||
const updatedColumns = config.columns?.map((col) =>
|
||||
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
||||
);
|
||||
|
||||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", {
|
||||
columnName: column.columnName,
|
||||
columnInfo: columnInfo,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// sourceTable과 joinTable이 모두 있어야 로드
|
||||
if (!sourceTable || !actualJoinTable) {
|
||||
console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
|
||||
const [sourceResult, joinResult] = await Promise.all([
|
||||
entityJoinApi.getReferenceTableColumns(sourceTable),
|
||||
entityJoinApi.getReferenceTableColumns(actualJoinTable),
|
||||
]);
|
||||
|
||||
const sourceColumns = sourceResult.columns || [];
|
||||
const joinColumns = joinResult.columns || [];
|
||||
|
||||
setEntityDisplayConfigs((prev) => ({
|
||||
...prev,
|
||||
[configKey]: {
|
||||
sourceColumns,
|
||||
joinColumns,
|
||||
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
||||
separator: column.entityDisplayConfig?.separator || " - ",
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 🎯 엔티티 표시 컬럼 선택 토글
|
||||
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
|
||||
const configKey = `${columnName}`;
|
||||
const localConfig = entityDisplayConfigs[configKey];
|
||||
if (!localConfig) return;
|
||||
|
||||
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
|
||||
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
|
||||
: [...localConfig.selectedColumns, selectedColumn];
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setEntityDisplayConfigs((prev) => ({
|
||||
...prev,
|
||||
[configKey]: {
|
||||
...prev[configKey],
|
||||
selectedColumns: newSelectedColumns,
|
||||
},
|
||||
}));
|
||||
|
||||
// 실제 컬럼 설정도 업데이트
|
||||
const updatedColumns = config.columns?.map((col) => {
|
||||
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||||
return {
|
||||
...col,
|
||||
entityDisplayConfig: {
|
||||
...col.entityDisplayConfig,
|
||||
displayColumns: newSelectedColumns,
|
||||
},
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", {
|
||||
columnName,
|
||||
selectedColumns: newSelectedColumns,
|
||||
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 🎯 엔티티 표시 구분자 업데이트
|
||||
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
|
||||
const configKey = `${columnName}`;
|
||||
const localConfig = entityDisplayConfigs[configKey];
|
||||
if (!localConfig) return;
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setEntityDisplayConfigs((prev) => ({
|
||||
...prev,
|
||||
[configKey]: {
|
||||
...prev[configKey],
|
||||
separator,
|
||||
},
|
||||
}));
|
||||
|
||||
// 실제 컬럼 설정도 업데이트
|
||||
const updatedColumns = config.columns?.map((col) => {
|
||||
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||||
return {
|
||||
...col,
|
||||
entityDisplayConfig: {
|
||||
...col.entityDisplayConfig,
|
||||
separator,
|
||||
},
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
console.log("🎯 엔티티 표시 구분자 설정 업데이트:", {
|
||||
columnName,
|
||||
separator,
|
||||
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 컬럼 순서 변경
|
||||
const moveColumn = (columnName: string, direction: "up" | "down") => {
|
||||
const columns = [...(config.columns || [])];
|
||||
|
|
@ -296,7 +653,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
if (!column) return;
|
||||
|
||||
// tableColumns에서 해당 컬럼의 메타정보 찾기
|
||||
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName || tc.column_name === columnName);
|
||||
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName);
|
||||
|
||||
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
|
||||
const inferWidgetType = (dataType: string, webType?: string): string => {
|
||||
|
|
@ -690,6 +1047,135 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
{/* 컬럼 설정 탭 */}
|
||||
<TabsContent value="columns" className="space-y-4">
|
||||
<ScrollArea className="h-[600px] pr-4">
|
||||
{/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */}
|
||||
{config.columns?.some((col) => col.isEntityJoin) && (
|
||||
<Card className="border-l-4 border-l-orange-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">🎯 엔티티 컬럼 표시 설정</CardTitle>
|
||||
<CardDescription>엔티티 타입 컬럼의 표시할 컬럼들을 조합하여 설정하세요</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{config.columns
|
||||
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
|
||||
.map((column) => (
|
||||
<div key={column.columnName} className="space-y-3 rounded-lg border bg-orange-50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
||||
{column.columnName}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{column.displayName}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => loadEntityDisplayConfig(column)}
|
||||
className="h-6 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
컬럼 로드
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{entityDisplayConfigs[column.columnName] && (
|
||||
<div className="space-y-3">
|
||||
{/* 구분자 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">구분자</Label>
|
||||
<Input
|
||||
value={entityDisplayConfigs[column.columnName].separator}
|
||||
onChange={(e) => updateEntityDisplaySeparator(column.columnName, e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder=" - "
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기본 테이블 컬럼 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-blue-600">
|
||||
기본 테이블: {column.entityDisplayConfig?.sourceTable}
|
||||
</Label>
|
||||
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
||||
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
|
||||
<div key={col.columnName} className="flex items-center space-x-1">
|
||||
<Checkbox
|
||||
id={`source-${column.columnName}-${col.columnName}`}
|
||||
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
||||
col.columnName,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`source-${column.columnName}-${col.columnName}`}
|
||||
className="flex-1 cursor-pointer text-xs"
|
||||
>
|
||||
{col.displayName}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조인 테이블 컬럼 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-green-600">
|
||||
조인 테이블: {column.entityDisplayConfig?.joinTable}
|
||||
</Label>
|
||||
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
||||
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
|
||||
<div key={col.columnName} className="flex items-center space-x-1">
|
||||
<Checkbox
|
||||
id={`join-${column.columnName}-${col.columnName}`}
|
||||
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
||||
col.columnName,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`join-${column.columnName}-${col.columnName}`}
|
||||
className="flex-1 cursor-pointer text-xs"
|
||||
>
|
||||
{col.displayName}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 선택된 컬럼 미리보기 */}
|
||||
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">미리보기</Label>
|
||||
<div className="flex flex-wrap gap-1 rounded bg-gray-50 p-2 text-xs">
|
||||
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
|
||||
<React.Fragment key={colName}>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{colName}
|
||||
</Badge>
|
||||
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
|
||||
<span className="text-gray-400">
|
||||
{entityDisplayConfigs[column.columnName].separator}
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!screenTableName ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
|
|
@ -820,6 +1306,20 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 엔티티 타입 컬럼 표시 */}
|
||||
{column.isEntityJoin && (
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center gap-2 rounded bg-orange-50 p-2">
|
||||
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
||||
엔티티 타입
|
||||
</Badge>
|
||||
<span className="text-xs text-orange-600">
|
||||
표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">정렬</Label>
|
||||
<Select
|
||||
|
|
@ -1018,7 +1518,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => addEntityJoinColumn(matchingJoinColumn)}
|
||||
onClick={() => addEntityColumn(matchingJoinColumn)}
|
||||
className="text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
|
|
@ -1057,7 +1557,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
key={index}
|
||||
variant={isAlreadyAdded ? "secondary" : "outline"}
|
||||
className="cursor-pointer text-xs"
|
||||
onClick={() => !isAlreadyAdded && addEntityJoinColumn(column)}
|
||||
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
|
||||
>
|
||||
{column.columnLabel}
|
||||
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,27 @@ export class TableListRenderer extends AutoRegisteringComponentRenderer {
|
|||
static componentDefinition = TableListDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <TableListComponent {...this.props} renderer={this} />;
|
||||
return <TableListComponent
|
||||
{...this.props}
|
||||
renderer={this}
|
||||
onConfigChange={this.handleConfigChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
// 설정 변경 핸들러
|
||||
protected handleConfigChange = (config: any) => {
|
||||
console.log("📥 TableListRenderer에서 설정 변경 받음:", config);
|
||||
|
||||
// 상위 컴포넌트의 onConfigChange 호출 (화면 설계자에게 알림)
|
||||
if (this.props.onConfigChange) {
|
||||
this.props.onConfigChange(config);
|
||||
} else {
|
||||
console.log("⚠️ 상위 컴포넌트에서 onConfigChange가 전달되지 않음");
|
||||
}
|
||||
|
||||
this.updateComponent({ config });
|
||||
};
|
||||
|
||||
/**
|
||||
* 컴포넌트별 특화 메서드들
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ export interface ColumnConfig {
|
|||
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
|
||||
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
|
||||
|
||||
// 🎯 엔티티 컬럼 표시 설정 (화면별 동적 설정)
|
||||
entityDisplayConfig?: {
|
||||
displayColumns: string[]; // 표시할 컬럼들 (기본 테이블 + 조인 테이블)
|
||||
separator?: string; // 구분자 (기본: " - ")
|
||||
sourceTable?: string; // 기본 테이블명
|
||||
joinTable?: string; // 조인 테이블명
|
||||
};
|
||||
|
||||
// 컬럼 고정 관련 속성
|
||||
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
|
||||
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const TestInputComponent: React.FC<TestInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
|||
const textStyle: React.CSSProperties = {
|
||||
fontSize: componentConfig.fontSize || "14px",
|
||||
fontWeight: componentConfig.fontWeight || "normal",
|
||||
color: componentConfig.color || "#374151",
|
||||
color: componentConfig.color || "#3b83f6",
|
||||
textAlign: componentConfig.textAlign || "left",
|
||||
backgroundColor: componentConfig.backgroundColor || "transparent",
|
||||
padding: componentConfig.padding || "0",
|
||||
|
|
@ -102,7 +102,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const TextDisplayConfigPanel: React.FC<TextDisplayConfigPanelProps> = ({
|
|||
<Input
|
||||
id="color"
|
||||
type="color"
|
||||
value={config.color || "#374151"}
|
||||
value={config.color || "#3b83f6"}
|
||||
onChange={(e) => handleChange("color", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({
|
|||
text: "텍스트를 입력하세요",
|
||||
fontSize: "14px",
|
||||
fontWeight: "normal",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
textAlign: "left",
|
||||
},
|
||||
defaultSize: { width: 150, height: 24 },
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
|||
</div>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ export class AutoRegisteringLayoutRenderer {
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "4px",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,8 +60,15 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
|
|||
|
||||
// 디자인 모드일 때 더 강조된 스타일
|
||||
if (isDesignMode) {
|
||||
zoneStyle.border = "2px dashed #cbd5e1";
|
||||
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
|
||||
// 🎯 컴포넌트가 있는 존은 테두리 제거 (컴포넌트 자체 테두리와 충돌 방지)
|
||||
if (zoneChildren.length === 0) {
|
||||
zoneStyle.border = "2px dashed #cbd5e1";
|
||||
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
|
||||
} else {
|
||||
// 컴포넌트가 있는 존은 미묘한 배경만
|
||||
zoneStyle.border = "1px solid transparent";
|
||||
zoneStyle.backgroundColor = "rgba(248, 250, 252, 0.3)";
|
||||
}
|
||||
}
|
||||
|
||||
// 호버 효과를 위한 추가 스타일
|
||||
|
|
@ -91,14 +98,26 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
|
|||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const element = e.currentTarget;
|
||||
element.style.borderColor = "#3b82f6";
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.02)";
|
||||
element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
|
||||
// 🎯 컴포넌트가 있는 존은 호버 효과 최소화
|
||||
if (zoneChildren.length > 0) {
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.01)";
|
||||
} else {
|
||||
element.style.borderColor = "#3b82f6";
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.02)";
|
||||
element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const element = e.currentTarget;
|
||||
element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)";
|
||||
if (zoneChildren.length > 0) {
|
||||
// 컴포넌트가 있는 존 복원
|
||||
element.style.borderColor = "transparent";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(248, 250, 252, 0.3)" : "rgba(248, 250, 252, 0.5)";
|
||||
} else {
|
||||
// 빈 존 복원
|
||||
element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)";
|
||||
}
|
||||
element.style.boxShadow = "none";
|
||||
}}
|
||||
onDrop={this.handleDrop(zone.id)}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const AccordionSection: React.FC<{
|
|||
const headerStyle: React.CSSProperties = {
|
||||
padding: "12px 16px",
|
||||
backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc",
|
||||
color: isDesignMode ? "white" : "#374151",
|
||||
color: isDesignMode ? "white" : "#3b83f6",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderBottom: isExpanded ? "none" : "1px solid #e2e8f0",
|
||||
cursor: "pointer",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = [];
|
|||
* Hot Reload 시스템 초기화
|
||||
*/
|
||||
export function initializeHotReload(): void {
|
||||
// 핫 리로드 시스템 임시 비활성화 (디버깅 목적)
|
||||
console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)");
|
||||
return;
|
||||
|
||||
if (process.env.NODE_ENV !== "development" || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -55,11 +59,15 @@ function setupDevServerEventListener(): void {
|
|||
const originalLog = console.log;
|
||||
let reloadPending = false;
|
||||
|
||||
// console.log 메시지를 감지하여 Hot Reload 트리거
|
||||
// console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만)
|
||||
console.log = (...args: any[]) => {
|
||||
const message = args.join(" ");
|
||||
|
||||
if (message.includes("compiled") || message.includes("Fast Refresh") || message.includes("component")) {
|
||||
// 핫 리로드를 트리거할 특정 메시지만 감지 (디버깅 로그는 제외)
|
||||
if ((message.includes("compiled") || message.includes("Fast Refresh")) &&
|
||||
!message.includes("🔍") && !message.includes("🎯") && !message.includes("📤") &&
|
||||
!message.includes("📥") && !message.includes("⚠️") && !message.includes("🔄") &&
|
||||
!message.includes("✅") && !message.includes("🔧") && !message.includes("📋")) {
|
||||
if (!reloadPending) {
|
||||
reloadPending = true;
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
screenTableName,
|
||||
tableColumns,
|
||||
}) => {
|
||||
console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`);
|
||||
|
||||
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
|
@ -180,10 +182,21 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
);
|
||||
}
|
||||
|
||||
console.log(`🔧 DynamicComponentConfigPanel 렌더링:`, {
|
||||
componentId,
|
||||
ConfigPanelComponent: ConfigPanelComponent?.name,
|
||||
config,
|
||||
configType: typeof config,
|
||||
configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
|
||||
screenTableName,
|
||||
tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={onChange}
|
||||
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
|
||||
screenTableName={screenTableName}
|
||||
tableColumns={tableColumns}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ export interface ComponentRendererProps {
|
|||
// 새로운 기능들
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
|
||||
// 설정 변경 핸들러
|
||||
onConfigChange?: (config: any) => void;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = {
|
|||
[ComponentCategory.CHART]: {
|
||||
name: "차트",
|
||||
description: "데이터 시각화 컴포넌트",
|
||||
color: "#06b6d4",
|
||||
color: "#3b83f6",
|
||||
},
|
||||
[ComponentCategory.FORM]: {
|
||||
name: "폼",
|
||||
|
|
@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = {
|
|||
[ComponentCategory.CONTAINER]: {
|
||||
name: "컨테이너",
|
||||
description: "다른 컴포넌트를 담는 컨테이너",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
},
|
||||
[ComponentCategory.SYSTEM]: {
|
||||
name: "시스템",
|
||||
|
|
|
|||
|
|
@ -192,10 +192,13 @@ export interface FileTypeConfig {
|
|||
export interface EntityTypeConfig {
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string;
|
||||
displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경
|
||||
displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated)
|
||||
searchColumns?: string[];
|
||||
filters?: Record<string, unknown>;
|
||||
placeholder?: string;
|
||||
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
||||
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export interface UnifiedColumnInfo {
|
|||
|
||||
// 입력 설정
|
||||
inputType: "direct" | "auto";
|
||||
input_type?: string; // 🎯 데이터베이스의 input_type 필드 (entity, text, number 등)
|
||||
detailSettings?: Record<string, unknown>; // JSON 파싱된 객체
|
||||
description?: string;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue