Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
94846e92ef
|
|
@ -51,21 +51,26 @@ router.get(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 회사 코드 추출 (멀티테넌시 필터링)
|
||||||
|
const userCompany = req.user?.companyCode;
|
||||||
|
|
||||||
console.log(`🔗 조인 데이터 조회:`, {
|
console.log(`🔗 조인 데이터 조회:`, {
|
||||||
leftTable,
|
leftTable,
|
||||||
rightTable,
|
rightTable,
|
||||||
leftColumn,
|
leftColumn,
|
||||||
rightColumn,
|
rightColumn,
|
||||||
leftValue,
|
leftValue,
|
||||||
|
userCompany,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 조인 데이터 조회
|
// 조인 데이터 조회 (회사 코드 전달)
|
||||||
const result = await dataService.getJoinedData(
|
const result = await dataService.getJoinedData(
|
||||||
leftTable as string,
|
leftTable as string,
|
||||||
rightTable as string,
|
rightTable as string,
|
||||||
leftColumn as string,
|
leftColumn as string,
|
||||||
rightColumn as string,
|
rightColumn as string,
|
||||||
leftValue as string
|
leftValue as string,
|
||||||
|
userCompany
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -352,8 +357,25 @@ router.post(
|
||||||
|
|
||||||
console.log(`➕ 레코드 생성: ${tableName}`, data);
|
console.log(`➕ 레코드 생성: ${tableName}`, data);
|
||||||
|
|
||||||
|
// company_code와 company_name 자동 추가 (멀티테넌시)
|
||||||
|
const enrichedData = { ...data };
|
||||||
|
|
||||||
|
// 테이블에 company_code 컬럼이 있는지 확인하고 자동으로 추가
|
||||||
|
const hasCompanyCode = await dataService.checkColumnExists(tableName, "company_code");
|
||||||
|
if (hasCompanyCode && req.user?.companyCode) {
|
||||||
|
enrichedData.company_code = req.user.companyCode;
|
||||||
|
console.log(`🏢 company_code 자동 추가: ${req.user.companyCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블에 company_name 컬럼이 있는지 확인하고 자동으로 추가
|
||||||
|
const hasCompanyName = await dataService.checkColumnExists(tableName, "company_name");
|
||||||
|
if (hasCompanyName && req.user?.companyName) {
|
||||||
|
enrichedData.company_name = req.user.companyName;
|
||||||
|
console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 레코드 생성
|
// 레코드 생성
|
||||||
const result = await dataService.createRecord(tableName, data);
|
const result = await dataService.createRecord(tableName, enrichedData);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(400).json(result);
|
return res.status(400).json(result);
|
||||||
|
|
@ -437,6 +459,58 @@ router.put(
|
||||||
* 레코드 삭제 API
|
* 레코드 삭제 API
|
||||||
* DELETE /api/data/{tableName}/{id}
|
* DELETE /api/data/{tableName}/{id}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 복합키 레코드 삭제 API (POST)
|
||||||
|
* POST /api/data/:tableName/delete
|
||||||
|
* Body: { user_id: 'xxx', dept_code: 'yyy' }
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/:tableName/delete",
|
||||||
|
authenticateToken,
|
||||||
|
async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
const compositeKey = req.body;
|
||||||
|
|
||||||
|
// 입력값 검증
|
||||||
|
if (!tableName || typeof tableName !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: "INVALID_TABLE_NAME",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "유효하지 않은 테이블명입니다.",
|
||||||
|
error: "INVALID_TABLE_NAME",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🗑️ 복합키 레코드 삭제: ${tableName}`, compositeKey);
|
||||||
|
|
||||||
|
// 레코드 삭제 (복합키 객체 전달)
|
||||||
|
const result = await dataService.deleteRecord(tableName, compositeKey);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(400).json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 레코드 삭제 성공: ${tableName}`);
|
||||||
|
return res.json(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`레코드 삭제 오류 (${req.params.tableName}):`, error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "레코드 삭제 중 오류가 발생했습니다.",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
"/:tableName/:id",
|
"/:tableName/:id",
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
|
|
|
||||||
|
|
@ -165,12 +165,13 @@ export class AuthService {
|
||||||
const authNames = authResult.map((row) => row.auth_name).join(",");
|
const authNames = authResult.map((row) => row.auth_name).join(",");
|
||||||
|
|
||||||
// 3. 회사 정보 조회 (Raw Query 전환)
|
// 3. 회사 정보 조회 (Raw Query 전환)
|
||||||
// Note: 현재 회사 정보는 PersonBean에 직접 사용되지 않지만 향후 확장을 위해 유지
|
|
||||||
const companyResult = await query<{ company_name: string }>(
|
const companyResult = await query<{ company_name: string }>(
|
||||||
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
||||||
[userInfo.company_code || "ILSHIN"]
|
[userInfo.company_code || "ILSHIN"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const companyName = companyResult.length > 0 ? companyResult[0].company_name : undefined;
|
||||||
|
|
||||||
// DB에서 조회한 원본 사용자 정보 상세 로그
|
// DB에서 조회한 원본 사용자 정보 상세 로그
|
||||||
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
||||||
// userId: userInfo.user_id,
|
// userId: userInfo.user_id,
|
||||||
|
|
@ -205,6 +206,7 @@ export class AuthService {
|
||||||
partnerObjid: userInfo.partner_objid || undefined,
|
partnerObjid: userInfo.partner_objid || undefined,
|
||||||
authName: authNames || undefined,
|
authName: authNames || undefined,
|
||||||
companyCode: companyCode,
|
companyCode: companyCode,
|
||||||
|
companyName: companyName, // 회사명 추가
|
||||||
photo: userInfo.photo
|
photo: userInfo.photo
|
||||||
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
|
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,9 @@ class DataService {
|
||||||
|
|
||||||
const columns = await this.getTableColumnsSimple(tableName);
|
const columns = await this.getTableColumnsSimple(tableName);
|
||||||
|
|
||||||
|
// PK 컬럼 정보 조회
|
||||||
|
const pkColumns = await this.getPrimaryKeyColumns(tableName);
|
||||||
|
|
||||||
// 컬럼 라벨 정보 추가
|
// 컬럼 라벨 정보 추가
|
||||||
const columnsWithLabels = await Promise.all(
|
const columnsWithLabels = await Promise.all(
|
||||||
columns.map(async (column) => {
|
columns.map(async (column) => {
|
||||||
|
|
@ -244,6 +247,7 @@ class DataService {
|
||||||
dataType: column.data_type,
|
dataType: column.data_type,
|
||||||
isNullable: column.is_nullable === "YES",
|
isNullable: column.is_nullable === "YES",
|
||||||
defaultValue: column.column_default,
|
defaultValue: column.column_default,
|
||||||
|
isPrimaryKey: pkColumns.includes(column.column_name), // PK 여부 추가
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -262,6 +266,26 @@ class DataService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블의 Primary Key 컬럼 목록 조회
|
||||||
|
*/
|
||||||
|
private async getPrimaryKeyColumns(tableName: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const result = await query<{ attname: string }>(
|
||||||
|
`SELECT a.attname
|
||||||
|
FROM pg_index i
|
||||||
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
|
||||||
|
[tableName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.map((row) => row.attname);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`PK 컬럼 조회 오류 (${tableName}):`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 존재 여부 확인
|
* 테이블 존재 여부 확인
|
||||||
*/
|
*/
|
||||||
|
|
@ -286,7 +310,7 @@ class DataService {
|
||||||
/**
|
/**
|
||||||
* 특정 컬럼 존재 여부 확인
|
* 특정 컬럼 존재 여부 확인
|
||||||
*/
|
*/
|
||||||
private async checkColumnExists(
|
async checkColumnExists(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string
|
columnName: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
|
@ -409,7 +433,8 @@ class DataService {
|
||||||
rightTable: string,
|
rightTable: string,
|
||||||
leftColumn: string,
|
leftColumn: string,
|
||||||
rightColumn: string,
|
rightColumn: string,
|
||||||
leftValue?: string | number
|
leftValue?: string | number,
|
||||||
|
userCompany?: string
|
||||||
): Promise<ServiceResponse<any[]>> {
|
): Promise<ServiceResponse<any[]>> {
|
||||||
try {
|
try {
|
||||||
// 왼쪽 테이블 접근 검증
|
// 왼쪽 테이블 접근 검증
|
||||||
|
|
@ -425,18 +450,42 @@ class DataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let queryText = `
|
let queryText = `
|
||||||
SELECT r.*
|
SELECT DISTINCT r.*
|
||||||
FROM "${rightTable}" r
|
FROM "${rightTable}" r
|
||||||
INNER JOIN "${leftTable}" l
|
INNER JOIN "${leftTable}" l
|
||||||
ON l."${leftColumn}" = r."${rightColumn}"
|
ON l."${leftColumn}" = r."${rightColumn}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
|
const whereConditions: string[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// 좌측 값 필터링
|
||||||
if (leftValue !== undefined && leftValue !== null) {
|
if (leftValue !== undefined && leftValue !== null) {
|
||||||
queryText += ` WHERE l."${leftColumn}" = $1`;
|
whereConditions.push(`l."${leftColumn}" = $${paramIndex}`);
|
||||||
values.push(leftValue);
|
values.push(leftValue);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 우측 테이블 회사별 필터링 (company_code 컬럼이 있는 경우)
|
||||||
|
if (userCompany && userCompany !== "*") {
|
||||||
|
const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code");
|
||||||
|
if (hasCompanyCode) {
|
||||||
|
whereConditions.push(`r.company_code = $${paramIndex}`);
|
||||||
|
values.push(userCompany);
|
||||||
|
paramIndex++;
|
||||||
|
console.log(`🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHERE 절 추가
|
||||||
|
if (whereConditions.length > 0) {
|
||||||
|
queryText += ` WHERE ${whereConditions.join(" AND ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 조인 쿼리 실행:", queryText);
|
||||||
|
console.log("📊 조인 쿼리 파라미터:", values);
|
||||||
|
|
||||||
const result = await query<any>(queryText, values);
|
const result = await query<any>(queryText, values);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -512,6 +561,11 @@ class DataService {
|
||||||
return validation.error!;
|
return validation.error!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _relationInfo 추출 (조인 관계 업데이트용)
|
||||||
|
const relationInfo = data._relationInfo;
|
||||||
|
const cleanData = { ...data };
|
||||||
|
delete cleanData._relationInfo;
|
||||||
|
|
||||||
// Primary Key 컬럼 찾기
|
// Primary Key 컬럼 찾기
|
||||||
const pkResult = await query<{ attname: string }>(
|
const pkResult = await query<{ attname: string }>(
|
||||||
`SELECT a.attname
|
`SELECT a.attname
|
||||||
|
|
@ -526,8 +580,8 @@ class DataService {
|
||||||
pkColumn = pkResult[0].attname;
|
pkColumn = pkResult[0].attname;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = Object.keys(data);
|
const columns = Object.keys(cleanData);
|
||||||
const values = Object.values(data);
|
const values = Object.values(cleanData);
|
||||||
const setClause = columns
|
const setClause = columns
|
||||||
.map((col, index) => `"${col}" = $${index + 1}`)
|
.map((col, index) => `"${col}" = $${index + 1}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
@ -550,6 +604,35 @@ class DataService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔗 조인 관계가 있는 경우, 연결된 테이블의 FK도 업데이트
|
||||||
|
if (relationInfo && relationInfo.rightTable && relationInfo.leftColumn && relationInfo.rightColumn) {
|
||||||
|
const { rightTable, leftColumn, rightColumn, oldLeftValue } = relationInfo;
|
||||||
|
const newLeftValue = cleanData[leftColumn];
|
||||||
|
|
||||||
|
// leftColumn 값이 변경된 경우에만 우측 테이블 업데이트
|
||||||
|
if (newLeftValue !== undefined && newLeftValue !== oldLeftValue) {
|
||||||
|
console.log("🔗 조인 관계 FK 업데이트:", {
|
||||||
|
rightTable,
|
||||||
|
rightColumn,
|
||||||
|
oldValue: oldLeftValue,
|
||||||
|
newValue: newLeftValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateRelatedQuery = `
|
||||||
|
UPDATE "${rightTable}"
|
||||||
|
SET "${rightColumn}" = $1
|
||||||
|
WHERE "${rightColumn}" = $2
|
||||||
|
`;
|
||||||
|
const updateResult = await query(updateRelatedQuery, [newLeftValue, oldLeftValue]);
|
||||||
|
console.log(`✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료`);
|
||||||
|
} catch (relError) {
|
||||||
|
console.error("❌ 연결된 테이블 업데이트 실패:", relError);
|
||||||
|
// 연결된 테이블 업데이트 실패 시 롤백은 하지 않고 경고만 로그
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: result[0],
|
data: result[0],
|
||||||
|
|
@ -569,7 +652,7 @@ class DataService {
|
||||||
*/
|
*/
|
||||||
async deleteRecord(
|
async deleteRecord(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
id: string | number
|
id: string | number | Record<string, any>
|
||||||
): Promise<ServiceResponse<void>> {
|
): Promise<ServiceResponse<void>> {
|
||||||
try {
|
try {
|
||||||
// 테이블 접근 검증
|
// 테이블 접근 검증
|
||||||
|
|
@ -578,28 +661,53 @@ class DataService {
|
||||||
return validation.error!;
|
return validation.error!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary Key 컬럼 찾기
|
// Primary Key 컬럼 찾기 (복합키 지원)
|
||||||
const pkResult = await query<{ attname: string }>(
|
const pkResult = await query<{ attname: string }>(
|
||||||
`SELECT a.attname
|
`SELECT a.attname
|
||||||
FROM pg_index i
|
FROM pg_index i
|
||||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
|
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
||||||
|
ORDER BY a.attnum`,
|
||||||
[tableName]
|
[tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
let pkColumn = "id";
|
let whereClauses: string[] = [];
|
||||||
if (pkResult.length > 0) {
|
let params: any[] = [];
|
||||||
pkColumn = pkResult[0].attname;
|
|
||||||
|
if (pkResult.length > 1) {
|
||||||
|
// 복합키인 경우: id가 객체여야 함
|
||||||
|
console.log(`🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map(r => r.attname).join(', ')}]`);
|
||||||
|
|
||||||
|
if (typeof id === 'object' && !Array.isArray(id)) {
|
||||||
|
// id가 객체인 경우: { user_id: 'xxx', dept_code: 'yyy' }
|
||||||
|
pkResult.forEach((pk, index) => {
|
||||||
|
whereClauses.push(`"${pk.attname}" = $${index + 1}`);
|
||||||
|
params.push(id[pk.attname]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// id가 문자열/숫자인 경우: 첫 번째 PK만 사용 (하위 호환성)
|
||||||
|
whereClauses.push(`"${pkResult[0].attname}" = $1`);
|
||||||
|
params.push(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 단일키인 경우
|
||||||
|
const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id";
|
||||||
|
whereClauses.push(`"${pkColumn}" = $1`);
|
||||||
|
params.push(typeof id === 'object' ? id[pkColumn] : id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryText = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
|
const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(' AND ')}`;
|
||||||
await query<any>(queryText, [id]);
|
console.log(`🗑️ 삭제 쿼리:`, queryText, params);
|
||||||
|
|
||||||
|
const result = await query<any>(queryText, params);
|
||||||
|
|
||||||
|
console.log(`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`레코드 삭제 오류 (${tableName}/${id}):`, error);
|
console.error(`레코드 삭제 오류 (${tableName}):`, error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "레코드 삭제 중 오류가 발생했습니다.",
|
message: "레코드 삭제 중 오류가 발생했습니다.",
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export interface PersonBean {
|
||||||
partnerObjid?: string;
|
partnerObjid?: string;
|
||||||
authName?: string;
|
authName?: string;
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
|
companyName?: string; // 회사명 추가
|
||||||
photo?: string;
|
photo?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
// 권한 레벨 정보 (3단계 체계)
|
// 권한 레벨 정보 (3단계 체계)
|
||||||
|
|
@ -94,6 +95,7 @@ export interface JwtPayload {
|
||||||
userName: string;
|
userName: string;
|
||||||
deptName?: string;
|
deptName?: string;
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
|
companyName?: string; // 회사명 추가
|
||||||
userType?: string;
|
userType?: string;
|
||||||
userTypeName?: string;
|
userTypeName?: string;
|
||||||
iat?: number;
|
iat?: number;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export class JwtUtils {
|
||||||
userName: userInfo.userName,
|
userName: userInfo.userName,
|
||||||
deptName: userInfo.deptName,
|
deptName: userInfo.deptName,
|
||||||
companyCode: userInfo.companyCode,
|
companyCode: userInfo.companyCode,
|
||||||
|
companyName: userInfo.companyName, // 회사명 추가
|
||||||
userType: userInfo.userType,
|
userType: userInfo.userType,
|
||||||
userTypeName: userInfo.userTypeName,
|
userTypeName: userInfo.userTypeName,
|
||||||
};
|
};
|
||||||
|
|
@ -45,6 +46,7 @@ export class JwtUtils {
|
||||||
userName: decoded.userName,
|
userName: decoded.userName,
|
||||||
deptName: decoded.deptName,
|
deptName: decoded.deptName,
|
||||||
companyCode: decoded.companyCode,
|
companyCode: decoded.companyCode,
|
||||||
|
companyName: decoded.companyName, // 회사명 추가
|
||||||
userType: decoded.userType,
|
userType: decoded.userType,
|
||||||
userTypeName: decoded.userTypeName,
|
userTypeName: decoded.userTypeName,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,15 @@ body {
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button 기본 커서 스타일 */
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Dialog/Modal Overlay ===== */
|
/* ===== Dialog/Modal Overlay ===== */
|
||||||
/* Radix UI Dialog Overlay - 60% 불투명도 배경 */
|
/* Radix UI Dialog Overlay - 60% 불투명도 배경 */
|
||||||
[data-radix-dialog-overlay],
|
[data-radix-dialog-overlay],
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export const dataApi = {
|
||||||
*/
|
*/
|
||||||
createRecord: async (tableName: string, data: Record<string, any>): Promise<any> => {
|
createRecord: async (tableName: string, data: Record<string, any>): Promise<any> => {
|
||||||
const response = await apiClient.post(`/data/${tableName}`, data);
|
const response = await apiClient.post(`/data/${tableName}`, data);
|
||||||
return response.data?.data || response.data;
|
return response.data; // success, data, message 포함된 전체 응답 반환
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,15 +94,23 @@ export const dataApi = {
|
||||||
*/
|
*/
|
||||||
updateRecord: async (tableName: string, id: string | number, data: Record<string, any>): Promise<any> => {
|
updateRecord: async (tableName: string, id: string | number, data: Record<string, any>): Promise<any> => {
|
||||||
const response = await apiClient.put(`/data/${tableName}/${id}`, data);
|
const response = await apiClient.put(`/data/${tableName}/${id}`, data);
|
||||||
return response.data?.data || response.data;
|
return response.data; // success, data, message 포함된 전체 응답 반환
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 레코드 삭제
|
* 레코드 삭제
|
||||||
* @param tableName 테이블명
|
* @param tableName 테이블명
|
||||||
* @param id 레코드 ID
|
* @param id 레코드 ID 또는 복합키 객체
|
||||||
*/
|
*/
|
||||||
deleteRecord: async (tableName: string, id: string | number): Promise<void> => {
|
deleteRecord: async (tableName: string, id: string | number | Record<string, any>): Promise<any> => {
|
||||||
await apiClient.delete(`/data/${tableName}/${id}`);
|
// 복합키 객체인 경우 POST로 전달
|
||||||
|
if (typeof id === 'object' && !Array.isArray(id)) {
|
||||||
|
const response = await apiClient.post(`/data/${tableName}/delete`, id);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단일 ID인 경우 기존 방식
|
||||||
|
const response = await apiClient.delete(`/data/${tableName}/${id}`);
|
||||||
|
return response.data; // success, message 포함된 전체 응답 반환
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -771,7 +771,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🖼️ 대표 이미지 로드 시작:", file.realFileName);
|
// objid가 없거나 유효하지 않으면 로드 중단
|
||||||
|
if (!file.objid || file.objid === "0" || file.objid === "") {
|
||||||
|
console.warn("⚠️ 대표 이미지 로드 실패: objid가 없음", file);
|
||||||
|
setRepresentativeImageUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🖼️ 대표 이미지 로드 시작:", {
|
||||||
|
objid: file.objid,
|
||||||
|
fileName: file.realFileName,
|
||||||
|
});
|
||||||
|
|
||||||
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
|
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
|
||||||
const response = await apiClient.get(`/files/download/${file.objid}`, {
|
const response = await apiClient.get(`/files/download/${file.objid}`, {
|
||||||
|
|
@ -792,8 +802,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
setRepresentativeImageUrl(url);
|
setRepresentativeImageUrl(url);
|
||||||
console.log("✅ 대표 이미지 로드 성공:", url);
|
console.log("✅ 대표 이미지 로드 성공:", url);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("❌ 대표 이미지 로드 실패:", error);
|
console.error("❌ 대표 이미지 로드 실패:", {
|
||||||
|
file: file.realFileName,
|
||||||
|
objid: file.objid,
|
||||||
|
error: error?.response?.status || error?.message,
|
||||||
|
});
|
||||||
setRepresentativeImageUrl(null);
|
setRepresentativeImageUrl(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,7 +9,7 @@ import { Slider } from "@/components/ui/slider";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Check, ChevronsUpDown, ArrowRight } from "lucide-react";
|
import { Check, ChevronsUpDown, ArrowRight, Plus, X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SplitPanelLayoutConfig } from "./types";
|
import { SplitPanelLayoutConfig } from "./types";
|
||||||
import { TableInfo, ColumnInfo } from "@/types/screen";
|
import { TableInfo, ColumnInfo } from "@/types/screen";
|
||||||
|
|
@ -74,6 +74,61 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [screenTableName]);
|
}, [screenTableName]);
|
||||||
|
|
||||||
|
// 좌측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
|
||||||
|
useEffect(() => {
|
||||||
|
const leftTableName = config.leftPanel?.tableName || screenTableName;
|
||||||
|
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showAdd) {
|
||||||
|
const currentAddModalColumns = config.leftPanel?.addModalColumns || [];
|
||||||
|
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
|
||||||
|
|
||||||
|
// PK가 추가되었으면 업데이트
|
||||||
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
||||||
|
console.log(`🔄 좌측 패널: PK 컬럼 자동 추가 (${leftTableName})`);
|
||||||
|
updateLeftPanel({ addModalColumns: updatedColumns });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]);
|
||||||
|
|
||||||
|
// 좌측 패널 하위 항목 추가 모달 PK 자동 추가
|
||||||
|
useEffect(() => {
|
||||||
|
const leftTableName = config.leftPanel?.tableName || screenTableName;
|
||||||
|
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) {
|
||||||
|
const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
||||||
|
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
|
||||||
|
|
||||||
|
// PK가 추가되었으면 업데이트
|
||||||
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
||||||
|
console.log(`🔄 좌측 패널 하위 항목 추가: PK 컬럼 자동 추가 (${leftTableName})`);
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
addModalColumns: updatedColumns,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]);
|
||||||
|
|
||||||
|
// 우측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
|
||||||
|
useEffect(() => {
|
||||||
|
const rightTableName = config.rightPanel?.tableName;
|
||||||
|
if (rightTableName && loadedTableColumns[rightTableName] && config.rightPanel?.showAdd) {
|
||||||
|
const currentAddModalColumns = config.rightPanel?.addModalColumns || [];
|
||||||
|
const updatedColumns = ensurePrimaryKeysInAddModal(rightTableName, currentAddModalColumns);
|
||||||
|
|
||||||
|
// PK가 추가되었으면 업데이트
|
||||||
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
||||||
|
console.log(`🔄 우측 패널: PK 컬럼 자동 추가 (${rightTableName})`);
|
||||||
|
updateRightPanel({ addModalColumns: updatedColumns });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [config.rightPanel?.tableName, loadedTableColumns, config.rightPanel?.showAdd]);
|
||||||
|
|
||||||
// 테이블 컬럼 로드 함수
|
// 테이블 컬럼 로드 함수
|
||||||
const loadTableColumns = async (tableName: string) => {
|
const loadTableColumns = async (tableName: string) => {
|
||||||
if (loadedTableColumns[tableName] || loadingColumns[tableName]) {
|
if (loadedTableColumns[tableName] || loadingColumns[tableName]) {
|
||||||
|
|
@ -98,6 +153,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
||||||
columnDefault: col.columnDefault || col.column_default,
|
columnDefault: col.columnDefault || col.column_default,
|
||||||
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
||||||
|
isPrimaryKey: col.isPrimaryKey || false, // PK 여부 추가
|
||||||
codeCategory: col.codeCategory || col.code_category,
|
codeCategory: col.codeCategory || col.code_category,
|
||||||
codeValue: col.codeValue || col.code_value,
|
codeValue: col.codeValue || col.code_value,
|
||||||
}));
|
}));
|
||||||
|
|
@ -139,6 +195,44 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
onChange(newConfig);
|
onChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PK 컬럼을 추가 모달에 자동으로 포함시키는 함수
|
||||||
|
const ensurePrimaryKeysInAddModal = (
|
||||||
|
tableName: string,
|
||||||
|
existingColumns: Array<{ name: string; label: string; required?: boolean }> = []
|
||||||
|
) => {
|
||||||
|
const tableColumns = loadedTableColumns[tableName];
|
||||||
|
if (!tableColumns) {
|
||||||
|
console.warn(`⚠️ 테이블 ${tableName}의 컬럼 정보가 로드되지 않음`);
|
||||||
|
return existingColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PK 컬럼 찾기
|
||||||
|
const pkColumns = tableColumns.filter((col) => col.isPrimaryKey);
|
||||||
|
console.log(`🔑 테이블 ${tableName}의 PK 컬럼:`, pkColumns.map(c => c.columnName));
|
||||||
|
|
||||||
|
// 자동으로 처리되는 컬럼 (백엔드에서 자동 추가)
|
||||||
|
const autoHandledColumns = ['company_code', 'company_name'];
|
||||||
|
|
||||||
|
// 기존 컬럼 이름 목록
|
||||||
|
const existingColumnNames = existingColumns.map((col) => col.name);
|
||||||
|
|
||||||
|
// PK 컬럼을 맨 앞에 추가 (이미 있거나 자동 처리되는 컬럼은 제외)
|
||||||
|
const pkColumnsToAdd = pkColumns
|
||||||
|
.filter((col) => !existingColumnNames.includes(col.columnName))
|
||||||
|
.filter((col) => !autoHandledColumns.includes(col.columnName)) // 자동 처리 컬럼 제외
|
||||||
|
.map((col) => ({
|
||||||
|
name: col.columnName,
|
||||||
|
label: col.columnLabel || col.columnName,
|
||||||
|
required: true, // PK는 항상 필수
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (pkColumnsToAdd.length > 0) {
|
||||||
|
console.log(`✅ PK 컬럼 ${pkColumnsToAdd.length}개 자동 추가:`, pkColumnsToAdd.map(c => c.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...pkColumnsToAdd, ...existingColumns];
|
||||||
|
};
|
||||||
|
|
||||||
const updateLeftPanel = (updates: Partial<SplitPanelLayoutConfig["leftPanel"]>) => {
|
const updateLeftPanel = (updates: Partial<SplitPanelLayoutConfig["leftPanel"]>) => {
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
...config,
|
...config,
|
||||||
|
|
@ -268,6 +362,451 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
onCheckedChange={(checked) => updateLeftPanel({ showAdd: checked })}
|
onCheckedChange={(checked) => updateLeftPanel({ showAdd: checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>각 항목에 + 버튼</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.leftPanel?.showItemAddButton ?? false}
|
||||||
|
onCheckedChange={(checked) => updateLeftPanel({ showItemAddButton: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 항목별 + 버튼 설정 (하위 항목 추가) */}
|
||||||
|
{config.leftPanel?.showItemAddButton && (
|
||||||
|
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||||
|
<Label className="text-sm font-semibold">하위 항목 추가 설정</Label>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
+ 버튼 클릭 시 선택된 항목의 하위 항목을 추가합니다 (예: 부서 → 하위 부서)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 현재 항목의 값을 가져올 컬럼 (sourceColumn) */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">현재 항목 ID 컬럼</Label>
|
||||||
|
<p className="mb-2 text-[10px] text-gray-500">
|
||||||
|
선택된 항목의 어떤 컬럼 값을 사용할지 (예: dept_code)
|
||||||
|
</p>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{config.leftPanel?.itemAddConfig?.sourceColumn || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{leftTableColumns
|
||||||
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||||
|
.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
sourceColumn: value,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
config.leftPanel?.itemAddConfig?.sourceColumn === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 상위 항목 ID를 저장할 컬럼 (parentColumn) */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">상위 항목 저장 컬럼</Label>
|
||||||
|
<p className="mb-2 text-[10px] text-gray-500">
|
||||||
|
하위 항목에서 상위 항목 ID를 저장할 컬럼 (예: parent_dept_code)
|
||||||
|
</p>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{config.leftPanel?.itemAddConfig?.parentColumn || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{leftTableColumns
|
||||||
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||||
|
.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
parentColumn: value,
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
config.leftPanel?.itemAddConfig?.parentColumn === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 하위 항목 추가 모달 컬럼 설정 */}
|
||||||
|
<div className="space-y-2 rounded border border-blue-300 bg-white p-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs font-semibold">추가 모달 입력 컬럼</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const currentColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
||||||
|
const newColumns = [
|
||||||
|
...currentColumns,
|
||||||
|
{ name: "", label: "", required: false },
|
||||||
|
];
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
addModalColumns: newColumns,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-6 text-[10px]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
컬럼 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
하위 항목 추가 시 입력받을 필드를 선택하세요
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
|
||||||
|
<p className="text-[10px] text-gray-500">설정된 컬럼이 없습니다</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => {
|
||||||
|
const column = leftTableColumns.find(c => c.columnName === col.name);
|
||||||
|
const isPK = column?.isPrimaryKey || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md border p-2",
|
||||||
|
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPK && (
|
||||||
|
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
|
||||||
|
PK
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
disabled={isPK}
|
||||||
|
className="h-7 w-full justify-between text-[10px]"
|
||||||
|
>
|
||||||
|
{col.name || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{leftTableColumns
|
||||||
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||||
|
.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
name: value,
|
||||||
|
label: column.columnLabel || value,
|
||||||
|
};
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
addModalColumns: newColumns,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<label className="flex cursor-pointer items-center gap-1 text-[10px] text-gray-600">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={col.required ?? false}
|
||||||
|
disabled={isPK}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
required: e.target.checked,
|
||||||
|
};
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
addModalColumns: newColumns,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
필수
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isPK}
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = (config.leftPanel?.itemAddConfig?.addModalColumns || []).filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
updateLeftPanel({
|
||||||
|
itemAddConfig: {
|
||||||
|
...config.leftPanel?.itemAddConfig,
|
||||||
|
addModalColumns: newColumns,
|
||||||
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||||
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 좌측 패널 추가 모달 컬럼 설정 */}
|
||||||
|
{config.leftPanel?.showAdd && (
|
||||||
|
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-semibold">추가 모달 입력 컬럼</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const currentColumns = config.leftPanel?.addModalColumns || [];
|
||||||
|
const newColumns = [
|
||||||
|
...currentColumns,
|
||||||
|
{ name: "", label: "", required: false },
|
||||||
|
];
|
||||||
|
updateLeftPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
컬럼 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.leftPanel?.addModalColumns || []).length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(config.leftPanel?.addModalColumns || []).map((col, index) => {
|
||||||
|
// 현재 컬럼이 PK인지 확인
|
||||||
|
const column = leftTableColumns.find(c => c.columnName === col.name);
|
||||||
|
const isPK = column?.isPrimaryKey || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md border p-2",
|
||||||
|
isPK ? "bg-yellow-50 border-yellow-300" : "bg-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPK && (
|
||||||
|
<span className="text-[10px] font-semibold text-yellow-700 px-1.5 py-0.5 bg-yellow-200 rounded">
|
||||||
|
PK
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
disabled={isPK}
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{col.name || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{leftTableColumns
|
||||||
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||||
|
.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
name: value,
|
||||||
|
label: column.columnLabel || value,
|
||||||
|
};
|
||||||
|
updateLeftPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<label className="flex items-center gap-1 text-xs text-gray-600 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={col.required ?? false}
|
||||||
|
disabled={isPK}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
required: e.target.checked,
|
||||||
|
};
|
||||||
|
updateLeftPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
필수
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isPK}
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = (config.leftPanel?.addModalColumns || []).filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
updateLeftPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 패널 설정 */}
|
{/* 우측 패널 설정 */}
|
||||||
|
|
@ -467,6 +1006,357 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
onCheckedChange={(checked) => updateRightPanel({ showAdd: checked })}
|
onCheckedChange={(checked) => updateRightPanel({ showAdd: checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 우측 패널 표시 컬럼 설정 */}
|
||||||
|
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-semibold">표시할 컬럼 선택</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const currentColumns = config.rightPanel?.columns || [];
|
||||||
|
const newColumns = [
|
||||||
|
...currentColumns,
|
||||||
|
{ name: "", label: "", width: 100 },
|
||||||
|
];
|
||||||
|
updateRightPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
disabled={!config.rightPanel?.tableName}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
컬럼 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
우측 패널에 표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 선택된 컬럼 목록 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.rightPanel?.columns || []).length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-400">
|
||||||
|
컬럼을 추가하지 않으면 모든 컬럼이 표시됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(config.rightPanel?.columns || []).map((col, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-2 rounded-md border bg-white p-2"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{col.name || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{rightTableColumns.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
name: value,
|
||||||
|
label: column.columnLabel || value,
|
||||||
|
};
|
||||||
|
updateRightPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = (config.rightPanel?.columns || []).filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
updateRightPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측 패널 추가 모달 컬럼 설정 */}
|
||||||
|
{config.rightPanel?.showAdd && (
|
||||||
|
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-semibold">추가 모달 입력 컬럼</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const currentColumns = config.rightPanel?.addModalColumns || [];
|
||||||
|
const newColumns = [
|
||||||
|
...currentColumns,
|
||||||
|
{ name: "", label: "", required: false },
|
||||||
|
];
|
||||||
|
updateRightPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
disabled={!config.rightPanel?.tableName}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
컬럼 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.rightPanel?.addModalColumns || []).length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(config.rightPanel?.addModalColumns || []).map((col, index) => {
|
||||||
|
// 현재 컬럼이 PK인지 확인
|
||||||
|
const column = rightTableColumns.find(c => c.columnName === col.name);
|
||||||
|
const isPK = column?.isPrimaryKey || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md border p-2",
|
||||||
|
isPK ? "bg-yellow-50 border-yellow-300" : "bg-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPK && (
|
||||||
|
<span className="text-[10px] font-semibold text-yellow-700 px-1.5 py-0.5 bg-yellow-200 rounded">
|
||||||
|
PK
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
disabled={isPK}
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{col.name || "컬럼 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
{rightTableColumns
|
||||||
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||||
|
.map((column) => (
|
||||||
|
<CommandItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
onSelect={(value) => {
|
||||||
|
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
name: value,
|
||||||
|
label: column.columnLabel || value,
|
||||||
|
};
|
||||||
|
updateRightPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
<span className="ml-2 text-[10px] text-gray-500">
|
||||||
|
({column.columnName})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<label className="flex items-center gap-1 text-xs text-gray-600 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={col.required ?? false}
|
||||||
|
disabled={isPK}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
required: e.target.checked,
|
||||||
|
};
|
||||||
|
updateRightPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
필수
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isPK}
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = (config.rightPanel?.addModalColumns || []).filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
updateRightPanel({ addModalColumns: newColumns });
|
||||||
|
}}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 중계 테이블 설정 */}
|
||||||
|
<div className="space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-3 mt-3">
|
||||||
|
<Label className="text-sm font-semibold">중계 테이블 설정 (N:M 관계)</Label>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
중계 테이블을 사용하여 다대다 관계를 구현합니다
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-700">실제 저장할 테이블</Label>
|
||||||
|
<Input
|
||||||
|
value={config.rightPanel?.addConfig?.targetTable || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const addConfig = config.rightPanel?.addConfig || {};
|
||||||
|
updateRightPanel({
|
||||||
|
addConfig: {
|
||||||
|
...addConfig,
|
||||||
|
targetTable: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="예: user_dept"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-500">
|
||||||
|
데이터가 실제로 저장될 중계 테이블명
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-700">좌측 패널 컬럼</Label>
|
||||||
|
<Input
|
||||||
|
value={config.rightPanel?.addConfig?.leftPanelColumn || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const addConfig = config.rightPanel?.addConfig || {};
|
||||||
|
updateRightPanel({
|
||||||
|
addConfig: {
|
||||||
|
...addConfig,
|
||||||
|
leftPanelColumn: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="예: dept_code"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-500">
|
||||||
|
좌측 패널에서 선택한 항목의 어떤 컬럼값을 가져올지
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-700">중계 테이블 대상 컬럼</Label>
|
||||||
|
<Input
|
||||||
|
value={config.rightPanel?.addConfig?.targetColumn || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const addConfig = config.rightPanel?.addConfig || {};
|
||||||
|
updateRightPanel({
|
||||||
|
addConfig: {
|
||||||
|
...addConfig,
|
||||||
|
targetColumn: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="예: dept_code"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-500">
|
||||||
|
중계 테이블의 어떤 컬럼에 좌측값을 저장할지
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-700">자동 채움 컬럼 (JSON)</Label>
|
||||||
|
<textarea
|
||||||
|
value={JSON.stringify(config.rightPanel?.addConfig?.autoFillColumns || {}, null, 2)}
|
||||||
|
onChange={(e) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(e.target.value);
|
||||||
|
const addConfig = config.rightPanel?.addConfig || {};
|
||||||
|
updateRightPanel({
|
||||||
|
addConfig: {
|
||||||
|
...addConfig,
|
||||||
|
autoFillColumns: parsed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// JSON 파싱 오류는 무시 (입력 중)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='{ "is_primary": false }'
|
||||||
|
className="mt-1 h-20 w-full rounded-md border border-input bg-white px-3 py-2 text-xs font-mono"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-500">
|
||||||
|
자동으로 채워질 컬럼과 기본값 (예: is_primary: false)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 레이아웃 설정 */}
|
{/* 레이아웃 설정 */}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,34 @@ export interface SplitPanelLayoutConfig {
|
||||||
dataSource?: string; // API 엔드포인트
|
dataSource?: string; // API 엔드포인트
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
showAdd?: boolean;
|
showAdd?: boolean;
|
||||||
|
showEdit?: boolean; // 수정 버튼
|
||||||
|
showDelete?: boolean; // 삭제 버튼
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
}>;
|
}>;
|
||||||
|
// 추가 모달에서 입력받을 컬럼 설정
|
||||||
|
addModalColumns?: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
}>;
|
||||||
|
// 각 항목에 + 버튼 표시 (하위 항목 추가)
|
||||||
|
showItemAddButton?: boolean;
|
||||||
|
// + 버튼 클릭 시 하위 항목 추가를 위한 설정
|
||||||
|
itemAddConfig?: {
|
||||||
|
// 하위 항목 추가 모달에서 입력받을 컬럼
|
||||||
|
addModalColumns?: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
}>;
|
||||||
|
// 상위 항목의 ID를 저장할 컬럼 (예: parent_dept_code)
|
||||||
|
parentColumn: string;
|
||||||
|
// 현재 항목의 어떤 컬럼 값을 parentColumn에 넣을지 (예: dept_code)
|
||||||
|
sourceColumn: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 우측 패널 설정
|
// 우측 패널 설정
|
||||||
|
|
@ -24,18 +47,35 @@ export interface SplitPanelLayoutConfig {
|
||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
showAdd?: boolean;
|
showAdd?: boolean;
|
||||||
|
showEdit?: boolean; // 수정 버튼
|
||||||
|
showDelete?: boolean; // 삭제 버튼
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
}>;
|
}>;
|
||||||
|
// 추가 모달에서 입력받을 컬럼 설정
|
||||||
|
addModalColumns?: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
// 좌측 선택 항목과의 관계 설정
|
// 좌측 선택 항목과의 관계 설정
|
||||||
relation?: {
|
relation?: {
|
||||||
type: "join" | "detail"; // 관계 타입
|
type: "join" | "detail"; // 관계 타입
|
||||||
leftColumn?: string; // 좌측 테이블의 연결 컬럼
|
leftColumn?: string; // 좌측 테이블의 연결 컬럼
|
||||||
|
rightColumn?: string; // 우측 테이블의 연결 컬럼 (join용)
|
||||||
foreignKey?: string; // 우측 테이블의 외래키 컬럼명
|
foreignKey?: string; // 우측 테이블의 외래키 컬럼명
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 우측 패널 추가 시 중계 테이블 설정 (N:M 관계)
|
||||||
|
addConfig?: {
|
||||||
|
targetTable?: string; // 실제로 INSERT할 테이블 (중계 테이블)
|
||||||
|
autoFillColumns?: Record<string, any>; // 자동으로 채워질 컬럼과 기본값
|
||||||
|
leftPanelColumn?: string; // 좌측 패널의 어떤 컬럼값을 가져올지
|
||||||
|
targetColumn?: string; // targetTable의 어떤 컬럼에 넣을지
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 레이아웃 설정
|
// 레이아웃 설정
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue