Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs 2025-11-10 09:36:05 +09:00
commit 94846e92ef
11 changed files with 2136 additions and 86 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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: "레코드 삭제 중 오류가 발생했습니다.",

View File

@ -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;

View File

@ -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,
}; };

View File

@ -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],

View File

@ -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 포함된 전체 응답 반환
}, },
}; };

View File

@ -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);
} }
}, },

View File

@ -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>
{/* 레이아웃 설정 */} {/* 레이아웃 설정 */}

View File

@ -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의 어떤 컬럼에 넣을지
};
}; };
// 레이아웃 설정 // 레이아웃 설정