Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -5348,6 +5348,322 @@ export class ScreenManagementService {
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POP 레이아웃 관리 (모바일/태블릿)
|
||||
// v2.0: 4모드 레이아웃 지원 (태블릿 가로/세로, 모바일 가로/세로)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* POP v1 → v2 마이그레이션 (백엔드)
|
||||
* - 단일 sections 배열 → 4모드별 layouts + 공유 sections/components
|
||||
*/
|
||||
private migratePopV1ToV2(v1Data: any): any {
|
||||
console.log("POP v1 → v2 마이그레이션 시작");
|
||||
|
||||
// 기본 v2 구조
|
||||
const v2Data: any = {
|
||||
version: "pop-2.0",
|
||||
layouts: {
|
||||
tablet_landscape: { sectionPositions: {}, componentPositions: {} },
|
||||
tablet_portrait: { sectionPositions: {}, componentPositions: {} },
|
||||
mobile_landscape: { sectionPositions: {}, componentPositions: {} },
|
||||
mobile_portrait: { sectionPositions: {}, componentPositions: {} },
|
||||
},
|
||||
sections: {},
|
||||
components: {},
|
||||
dataFlow: {
|
||||
sectionConnections: [],
|
||||
},
|
||||
settings: {
|
||||
touchTargetMin: 48,
|
||||
mode: "normal",
|
||||
canvasGrid: v1Data.canvasGrid || { columns: 24, rowHeight: 20, gap: 4 },
|
||||
},
|
||||
metadata: v1Data.metadata,
|
||||
};
|
||||
|
||||
// v1 섹션 배열 처리
|
||||
const sections = v1Data.sections || [];
|
||||
const modeKeys = ["tablet_landscape", "tablet_portrait", "mobile_landscape", "mobile_portrait"];
|
||||
|
||||
for (const section of sections) {
|
||||
// 섹션 정의 생성
|
||||
v2Data.sections[section.id] = {
|
||||
id: section.id,
|
||||
label: section.label,
|
||||
componentIds: (section.components || []).map((c: any) => c.id),
|
||||
innerGrid: section.innerGrid || { columns: 3, rows: 3, gap: 4 },
|
||||
style: section.style,
|
||||
};
|
||||
|
||||
// 섹션 위치 복사 (4모드 모두 동일)
|
||||
const sectionPos = section.grid || { col: 1, row: 1, colSpan: 3, rowSpan: 4 };
|
||||
for (const mode of modeKeys) {
|
||||
v2Data.layouts[mode].sectionPositions[section.id] = { ...sectionPos };
|
||||
}
|
||||
|
||||
// 컴포넌트별 처리
|
||||
for (const comp of section.components || []) {
|
||||
// 컴포넌트 정의 생성
|
||||
v2Data.components[comp.id] = {
|
||||
id: comp.id,
|
||||
type: comp.type,
|
||||
label: comp.label,
|
||||
dataBinding: comp.dataBinding,
|
||||
style: comp.style,
|
||||
config: comp.config,
|
||||
};
|
||||
|
||||
// 컴포넌트 위치 복사 (4모드 모두 동일)
|
||||
const compPos = comp.grid || { col: 1, row: 1, colSpan: 1, rowSpan: 1 };
|
||||
for (const mode of modeKeys) {
|
||||
v2Data.layouts[mode].componentPositions[comp.id] = { ...compPos };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sectionCount = Object.keys(v2Data.sections).length;
|
||||
const componentCount = Object.keys(v2Data.components).length;
|
||||
console.log(`POP v1 → v2 마이그레이션 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
|
||||
|
||||
return v2Data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 조회
|
||||
* - screen_layouts_pop 테이블에서 화면당 1개 레코드 조회
|
||||
* - v1 데이터는 자동으로 v2로 마이그레이션하여 반환
|
||||
*/
|
||||
async getLayoutPop(
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
userType?: string,
|
||||
): Promise<any | null> {
|
||||
console.log(`=== POP 레이아웃 로드 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 사용자 유형: ${userType}`);
|
||||
|
||||
// SUPER_ADMIN 여부 확인
|
||||
const isSuperAdmin = userType === "SUPER_ADMIN";
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{
|
||||
company_code: string | null;
|
||||
table_name: string | null;
|
||||
}>(
|
||||
`SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
// SUPER_ADMIN이 아니고 회사 코드가 다르면 권한 없음
|
||||
if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 조회할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
let layout: { layout_data: any } | null = null;
|
||||
|
||||
// SUPER_ADMIN인 경우: 화면의 회사 코드로 레이아웃 조회
|
||||
if (isSuperAdmin) {
|
||||
// 1. 화면 정의의 회사 코드로 레이아웃 조회
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, existingScreen.company_code],
|
||||
);
|
||||
|
||||
// 2. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째 조회
|
||||
if (!layout) {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 일반 사용자: 회사별 우선, 없으면 공통(*) 조회
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
// 회사별 레이아웃이 없으면 공통(*) 레이아웃 조회
|
||||
if (!layout && companyCode !== "*") {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = '*'`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!layout) {
|
||||
console.log(`POP 레이아웃 없음: screen_id=${screenId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const layoutData = layout.layout_data;
|
||||
|
||||
// v1 → v2 자동 마이그레이션
|
||||
if (layoutData && layoutData.version === "pop-1.0") {
|
||||
console.log("POP v1 레이아웃 감지, v2로 마이그레이션");
|
||||
return this.migratePopV1ToV2(layoutData);
|
||||
}
|
||||
|
||||
// v2 또는 버전 태그 없는 경우 (버전 태그 없으면 sections 구조 확인)
|
||||
if (layoutData && !layoutData.version && layoutData.sections && Array.isArray(layoutData.sections)) {
|
||||
console.log("버전 태그 없는 v1 레이아웃 감지, v2로 마이그레이션");
|
||||
return this.migratePopV1ToV2({ ...layoutData, version: "pop-1.0" });
|
||||
}
|
||||
|
||||
// v2 레이아웃 그대로 반환
|
||||
const sectionCount = layoutData?.sections ? Object.keys(layoutData.sections).length : 0;
|
||||
const componentCount = layoutData?.components ? Object.keys(layoutData.components).length : 0;
|
||||
console.log(`POP v2 레이아웃 로드 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
|
||||
|
||||
return layoutData;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 저장
|
||||
* - screen_layouts_pop 테이블에 화면당 1개 레코드 저장
|
||||
* - v3 형식 지원 (version: "pop-3.0", 섹션 제거)
|
||||
* - v2/v1 하위 호환
|
||||
*/
|
||||
async saveLayoutPop(
|
||||
screenId: number,
|
||||
layoutData: any,
|
||||
companyCode: string,
|
||||
userId?: string,
|
||||
): Promise<void> {
|
||||
console.log(`=== POP 레이아웃 저장 (v5 그리드 시스템) ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
|
||||
|
||||
// v5 그리드 레이아웃만 지원
|
||||
const componentCount = Object.keys(layoutData.components || {}).length;
|
||||
console.log(`컴포넌트: ${componentCount}개`);
|
||||
|
||||
// v5 형식 검증
|
||||
if (layoutData.version && layoutData.version !== "pop-5.0") {
|
||||
console.warn(`레거시 버전 감지 (${layoutData.version}), v5로 변환 필요`);
|
||||
}
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null }>(
|
||||
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 저장할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// SUPER_ADMIN인 경우: 화면 정의의 company_code로 저장 (로드와 동일하게)
|
||||
const targetCompanyCode = companyCode === "*"
|
||||
? (existingScreen.company_code || "*")
|
||||
: companyCode;
|
||||
|
||||
console.log(`저장 대상 company_code: ${targetCompanyCode} (사용자: ${companyCode}, 화면: ${existingScreen.company_code})`);
|
||||
|
||||
// v5 그리드 레이아웃으로 저장 (단일 버전)
|
||||
const dataToSave = {
|
||||
...layoutData,
|
||||
version: "pop-5.0",
|
||||
};
|
||||
console.log(`저장: gridConfig=${JSON.stringify(dataToSave.gridConfig || 'default')}`)
|
||||
|
||||
// UPSERT (있으면 업데이트, 없으면 삽입)
|
||||
await query(
|
||||
`INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, NOW(), NOW(), $4, $4)
|
||||
ON CONFLICT (screen_id, company_code)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = $4`,
|
||||
[screenId, targetCompanyCode, JSON.stringify(dataToSave), userId || null],
|
||||
);
|
||||
|
||||
console.log(`POP 레이아웃 저장 완료 (version: ${dataToSave.version}, company: ${targetCompanyCode})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃이 존재하는 화면 ID 목록 조회
|
||||
* - 옵션 B: POP 레이아웃 존재 여부로 화면 구분
|
||||
*/
|
||||
async getScreenIdsWithPopLayout(
|
||||
companyCode: string,
|
||||
): Promise<number[]> {
|
||||
console.log(`=== POP 레이아웃 존재 화면 ID 조회 ===`);
|
||||
console.log(`회사 코드: ${companyCode}`);
|
||||
|
||||
let result: { screen_id: number }[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 POP 레이아웃 조회
|
||||
result = await query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id FROM screen_layouts_pop`,
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
// 일반 회사: 해당 회사 또는 공통(*) 레이아웃 조회
|
||||
result = await query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id FROM screen_layouts_pop
|
||||
WHERE company_code = $1 OR company_code = '*'`,
|
||||
[companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
const screenIds = result.map((r) => r.screen_id);
|
||||
console.log(`POP 레이아웃 존재 화면 수: ${screenIds.length}개`);
|
||||
return screenIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 삭제
|
||||
*/
|
||||
async deleteLayoutPop(
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
): Promise<boolean> {
|
||||
console.log(`=== POP 레이아웃 삭제 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null }>(
|
||||
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 삭제할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`DELETE FROM screen_layouts_pop WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
console.log(`POP 레이아웃 삭제 완료`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 서비스 인스턴스 export
|
||||
|
||||
Reference in New Issue
Block a user