분할 패널에서 부서 추가 기능 구현
This commit is contained in:
parent
7835898a09
commit
efaa267d78
|
|
@ -62,10 +62,10 @@ export async function getColumnList(
|
|||
try {
|
||||
const { tableName } = req.params;
|
||||
const { page = 1, size = 50 } = req.query;
|
||||
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
||||
|
||||
if (!companyCode && req.user?.userId) {
|
||||
// JWT에 없으면 DB에서 조회
|
||||
const { query } = require("../database/db");
|
||||
|
|
@ -74,7 +74,9 @@ export async function getColumnList(
|
|||
[req.user.userId]
|
||||
);
|
||||
companyCode = userResult[0]?.company_code;
|
||||
logger.info(`DB에서 회사 코드 조회 (컬럼 목록): ${req.user.userId} → ${companyCode}`);
|
||||
logger.info(
|
||||
`DB에서 회사 코드 조회 (컬럼 목록): ${req.user.userId} → ${companyCode}`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
|
@ -139,10 +141,10 @@ export async function updateColumnSettings(
|
|||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const settings: ColumnSettings = req.body;
|
||||
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
||||
|
||||
if (!companyCode && req.user?.userId) {
|
||||
// JWT에 없으면 DB에서 조회
|
||||
const { query } = require("../database/db");
|
||||
|
|
@ -154,7 +156,9 @@ export async function updateColumnSettings(
|
|||
logger.info(`DB에서 회사 코드 조회: ${req.user.userId} → ${companyCode}`);
|
||||
}
|
||||
|
||||
logger.info(`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode} ===`);
|
||||
logger.info(
|
||||
`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode} ===`
|
||||
);
|
||||
|
||||
if (!tableName || !columnName) {
|
||||
const response: ApiResponse<null> = {
|
||||
|
|
@ -194,7 +198,8 @@ export async function updateColumnSettings(
|
|||
message: "회사 코드를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "MISSING_COMPANY_CODE",
|
||||
details: "사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
details:
|
||||
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
|
|
@ -209,7 +214,9 @@ export async function updateColumnSettings(
|
|||
companyCode // 🔥 회사 코드 전달
|
||||
);
|
||||
|
||||
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}, company: ${companyCode}`);
|
||||
logger.info(
|
||||
`컬럼 설정 업데이트 완료: ${tableName}.${columnName}, company: ${companyCode}`
|
||||
);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
|
|
@ -243,10 +250,10 @@ export async function updateAllColumnSettings(
|
|||
try {
|
||||
const { tableName } = req.params;
|
||||
const columnSettings: ColumnSettings[] = req.body;
|
||||
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
||||
|
||||
if (!companyCode && req.user?.userId) {
|
||||
// JWT에 없으면 DB에서 조회
|
||||
const { query } = require("../database/db");
|
||||
|
|
@ -264,7 +271,9 @@ export async function updateAllColumnSettings(
|
|||
logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`);
|
||||
logger.info(`[DEBUG] companyCode 최종값: ${companyCode}`);
|
||||
|
||||
logger.info(`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, company: ${companyCode} ===`);
|
||||
logger.info(
|
||||
`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, company: ${companyCode} ===`
|
||||
);
|
||||
|
||||
if (!tableName) {
|
||||
const response: ApiResponse<null> = {
|
||||
|
|
@ -305,7 +314,8 @@ export async function updateAllColumnSettings(
|
|||
message: "회사 코드를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "MISSING_COMPANY_CODE",
|
||||
details: "사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
details:
|
||||
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
|
|
@ -543,10 +553,10 @@ export async function updateColumnInputType(
|
|||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { inputType, detailSettings } = req.body;
|
||||
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
||||
|
||||
if (!companyCode && req.user?.userId) {
|
||||
// JWT에 없으면 DB에서 조회
|
||||
const { query } = require("../database/db");
|
||||
|
|
@ -588,7 +598,8 @@ export async function updateColumnInputType(
|
|||
message: "회사 코드를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "MISSING_COMPANY_CODE",
|
||||
details: "사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
details:
|
||||
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
|
|
@ -1085,10 +1096,10 @@ export async function getColumnWebTypes(
|
|||
): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
||||
|
||||
if (!companyCode && req.user?.userId) {
|
||||
// JWT에 없으면 DB에서 조회
|
||||
const { query } = require("../database/db");
|
||||
|
|
@ -1097,7 +1108,9 @@ export async function getColumnWebTypes(
|
|||
[req.user.userId]
|
||||
);
|
||||
companyCode = userResult[0]?.company_code;
|
||||
logger.info(`DB에서 회사 코드 조회 (조회): ${req.user.userId} → ${companyCode}`);
|
||||
logger.info(
|
||||
`DB에서 회사 코드 조회 (조회): ${req.user.userId} → ${companyCode}`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
|
@ -1129,7 +1142,8 @@ export async function getColumnWebTypes(
|
|||
message: "회사 코드를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "MISSING_COMPANY_CODE",
|
||||
details: "사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
details:
|
||||
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
|
|
|
|||
|
|
@ -357,8 +357,25 @@ router.post(
|
|||
|
||||
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) {
|
||||
return res.status(400).json(result);
|
||||
|
|
|
|||
|
|
@ -165,12 +165,13 @@ export class AuthService {
|
|||
const authNames = authResult.map((row) => row.auth_name).join(",");
|
||||
|
||||
// 3. 회사 정보 조회 (Raw Query 전환)
|
||||
// Note: 현재 회사 정보는 PersonBean에 직접 사용되지 않지만 향후 확장을 위해 유지
|
||||
const companyResult = await query<{ company_name: string }>(
|
||||
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
||||
[userInfo.company_code || "ILSHIN"]
|
||||
);
|
||||
|
||||
const companyName = companyResult.length > 0 ? companyResult[0].company_name : undefined;
|
||||
|
||||
// DB에서 조회한 원본 사용자 정보 상세 로그
|
||||
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
||||
// userId: userInfo.user_id,
|
||||
|
|
@ -205,6 +206,7 @@ export class AuthService {
|
|||
partnerObjid: userInfo.partner_objid || undefined,
|
||||
authName: authNames || undefined,
|
||||
companyCode: companyCode,
|
||||
companyName: companyName, // 회사명 추가
|
||||
photo: userInfo.photo
|
||||
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
|
||||
: undefined,
|
||||
|
|
|
|||
|
|
@ -231,6 +231,9 @@ class DataService {
|
|||
|
||||
const columns = await this.getTableColumnsSimple(tableName);
|
||||
|
||||
// PK 컬럼 정보 조회
|
||||
const pkColumns = await this.getPrimaryKeyColumns(tableName);
|
||||
|
||||
// 컬럼 라벨 정보 추가
|
||||
const columnsWithLabels = await Promise.all(
|
||||
columns.map(async (column) => {
|
||||
|
|
@ -244,6 +247,7 @@ class DataService {
|
|||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable === "YES",
|
||||
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,
|
||||
columnName: string
|
||||
): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface PersonBean {
|
|||
partnerObjid?: string;
|
||||
authName?: string;
|
||||
companyCode?: string;
|
||||
companyName?: string; // 회사명 추가
|
||||
photo?: string;
|
||||
locale?: string;
|
||||
// 권한 레벨 정보 (3단계 체계)
|
||||
|
|
@ -94,6 +95,7 @@ export interface JwtPayload {
|
|||
userName: string;
|
||||
deptName?: string;
|
||||
companyCode?: string;
|
||||
companyName?: string; // 회사명 추가
|
||||
userType?: string;
|
||||
userTypeName?: string;
|
||||
iat?: number;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export class JwtUtils {
|
|||
userName: userInfo.userName,
|
||||
deptName: userInfo.deptName,
|
||||
companyCode: userInfo.companyCode,
|
||||
companyName: userInfo.companyName, // 회사명 추가
|
||||
userType: userInfo.userType,
|
||||
userTypeName: userInfo.userTypeName,
|
||||
};
|
||||
|
|
@ -45,6 +46,7 @@ export class JwtUtils {
|
|||
userName: decoded.userName,
|
||||
deptName: decoded.deptName,
|
||||
companyCode: decoded.companyCode,
|
||||
companyName: decoded.companyName, // 회사명 추가
|
||||
userType: decoded.userType,
|
||||
userTypeName: decoded.userTypeName,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const dataApi = {
|
|||
*/
|
||||
createRecord: async (tableName: string, data: Record<string, any>): Promise<any> => {
|
||||
const response = await apiClient.post(`/data/${tableName}`, data);
|
||||
return response.data?.data || response.data;
|
||||
return response.data; // success, data, message 포함된 전체 응답 반환
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { SplitPanelLayoutConfig } from "./types";
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save } from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||
// 추가 props
|
||||
|
|
@ -47,6 +49,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const [rightTableColumns, setRightTableColumns] = useState<any[]>([]); // 우측 테이블 컬럼 정보
|
||||
const { toast } = useToast();
|
||||
|
||||
// 추가 모달 상태
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | null>(null);
|
||||
const [addModalFormData, setAddModalFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 리사이저 드래그 상태
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [leftWidth, setLeftWidth] = useState(splitRatio);
|
||||
|
|
@ -208,6 +215,115 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
loadRightTableColumns();
|
||||
}, [componentConfig.rightPanel?.tableName, isDesignMode]);
|
||||
|
||||
// 추가 버튼 핸들러
|
||||
const handleAddClick = useCallback((panel: "left" | "right") => {
|
||||
setAddModalPanel(panel);
|
||||
setAddModalFormData({});
|
||||
setShowAddModal(true);
|
||||
}, []);
|
||||
|
||||
// 추가 모달 저장
|
||||
const handleAddModalSave = useCallback(async () => {
|
||||
const tableName = addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.tableName
|
||||
: componentConfig.rightPanel?.tableName;
|
||||
|
||||
const modalColumns = addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.addModalColumns
|
||||
: componentConfig.rightPanel?.addModalColumns;
|
||||
|
||||
if (!tableName) {
|
||||
toast({
|
||||
title: "테이블 오류",
|
||||
description: "테이블명이 설정되지 않았습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = (modalColumns || []).filter(col => col.required);
|
||||
for (const field of requiredFields) {
|
||||
if (!addModalFormData[field.name]) {
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: `${field.label}은(는) 필수 입력 항목입니다.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("📝 데이터 추가:", { tableName, data: addModalFormData });
|
||||
|
||||
const result = await dataApi.createRecord(tableName, addModalFormData);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "데이터가 성공적으로 추가되었습니다.",
|
||||
});
|
||||
|
||||
// 모달 닫기
|
||||
setShowAddModal(false);
|
||||
setAddModalFormData({});
|
||||
|
||||
// 데이터 새로고침
|
||||
if (addModalPanel === "left") {
|
||||
loadLeftData();
|
||||
} else if (selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: "저장 실패",
|
||||
description: result.message || "데이터 추가에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("데이터 추가 오류:", error);
|
||||
|
||||
// 에러 메시지 추출
|
||||
let errorMessage = "데이터 추가 중 오류가 발생했습니다.";
|
||||
|
||||
if (error?.response?.data) {
|
||||
const responseData = error.response.data;
|
||||
|
||||
// 백엔드에서 반환한 에러 메시지 확인
|
||||
if (responseData.error) {
|
||||
// 중복 키 에러 처리
|
||||
if (responseData.error.includes("duplicate key")) {
|
||||
errorMessage = "이미 존재하는 값입니다. 다른 값을 입력해주세요.";
|
||||
}
|
||||
// NOT NULL 제약조건 에러
|
||||
else if (responseData.error.includes("null value")) {
|
||||
const match = responseData.error.match(/column "(\w+)"/);
|
||||
const columnName = match ? match[1] : "필수";
|
||||
errorMessage = `${columnName} 필드는 필수 입력 항목입니다.`;
|
||||
}
|
||||
// 외래키 제약조건 에러
|
||||
else if (responseData.error.includes("foreign key")) {
|
||||
errorMessage = "참조하는 데이터가 존재하지 않습니다.";
|
||||
}
|
||||
// 기타 에러
|
||||
else {
|
||||
errorMessage = responseData.message || responseData.error;
|
||||
}
|
||||
} else if (responseData.message) {
|
||||
errorMessage = responseData.message;
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [addModalPanel, componentConfig, addModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]);
|
||||
|
||||
// 초기 데이터 로드
|
||||
useEffect(() => {
|
||||
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
||||
|
|
@ -295,8 +411,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
||||
</CardTitle>
|
||||
{componentConfig.leftPanel?.showAdd && (
|
||||
<Button size="sm" variant="outline">
|
||||
{componentConfig.leftPanel?.showAdd && !isDesignMode && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddClick("left")}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
|
|
@ -478,8 +598,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||
</CardTitle>
|
||||
{componentConfig.rightPanel?.showAdd && (
|
||||
<Button size="sm" variant="outline">
|
||||
{componentConfig.rightPanel?.showAdd && !isDesignMode && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddClick("right")}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
|
|
@ -712,6 +836,63 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 추가 모달 */}
|
||||
<Dialog open={showAddModal} onOpenChange={setShowAddModal}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
{addModalPanel === "left" ? componentConfig.leftPanel?.title : componentConfig.rightPanel?.title} 추가
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
새로운 데이터를 추가합니다. 필수 항목을 입력해주세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{(addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.addModalColumns
|
||||
: componentConfig.rightPanel?.addModalColumns
|
||||
)?.map((col, index) => (
|
||||
<div key={index}>
|
||||
<Label htmlFor={col.name} className="text-xs sm:text-sm">
|
||||
{col.label} {col.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={col.name}
|
||||
value={addModalFormData[col.name] || ""}
|
||||
onChange={(e) => {
|
||||
setAddModalFormData(prev => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${col.label} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
required={col.required}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddModalSave}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
<Save className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -74,6 +74,38 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 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) => {
|
||||
if (loadedTableColumns[tableName] || loadingColumns[tableName]) {
|
||||
|
|
@ -98,6 +130,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
||||
columnDefault: col.columnDefault || col.column_default,
|
||||
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
||||
isPrimaryKey: col.isPrimaryKey || false, // PK 여부 추가
|
||||
codeCategory: col.codeCategory || col.code_category,
|
||||
codeValue: col.codeValue || col.code_value,
|
||||
}));
|
||||
|
|
@ -139,6 +172,44 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
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 newConfig = {
|
||||
...config,
|
||||
|
|
@ -269,6 +340,149 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
/>
|
||||
</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>
|
||||
|
||||
{/* 우측 패널 설정 */}
|
||||
|
|
@ -577,6 +791,151 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 레이아웃 설정 */}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ export interface SplitPanelLayoutConfig {
|
|||
label: string;
|
||||
width?: number;
|
||||
}>;
|
||||
// 추가 모달에서 입력받을 컬럼 설정
|
||||
addModalColumns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
// 우측 패널 설정
|
||||
|
|
@ -29,6 +35,12 @@ export interface SplitPanelLayoutConfig {
|
|||
label: string;
|
||||
width?: number;
|
||||
}>;
|
||||
// 추가 모달에서 입력받을 컬럼 설정
|
||||
addModalColumns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
|
||||
// 좌측 선택 항목과의 관계 설정
|
||||
relation?: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue