feat: 멀티테넌시 지원을 위한 레이어 관리 기능 추가
- 레이어 목록 조회, 특정 레이어 레이아웃 조회, 레이어 삭제 및 조건 설정 업데이트 기능을 추가했습니다. - 엔티티 참조 데이터 조회 및 공통 코드 데이터 조회에 멀티테넌시 필터를 적용하여 인증된 사용자의 회사 코드에 따라 데이터 접근을 제한했습니다. - 레이어 관리 패널에서 기본 레이어와 조건부 레이어의 컴포넌트를 통합하여 조건부 영역의 표시를 개선했습니다. - 레이아웃 저장 시 레이어 ID를 포함하여 레이어별로 저장할 수 있도록 변경했습니다.
This commit is contained in:
@@ -4245,11 +4245,11 @@ export class ScreenManagementService {
|
||||
},
|
||||
);
|
||||
|
||||
// V2 레이아웃 저장 (UPSERT)
|
||||
// V2 레이아웃 저장 (UPSERT) - layer_id 포함
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code)
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, 1, $3, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW()`,
|
||||
[newScreen.screen_id, targetCompanyCode, JSON.stringify(updatedLayoutData)],
|
||||
);
|
||||
@@ -5073,38 +5073,63 @@ export class ScreenManagementService {
|
||||
|
||||
let layout: { layout_data: any } | null = null;
|
||||
|
||||
// 🆕 기본 레이어(layer_id=1)를 우선 로드
|
||||
// SUPER_ADMIN인 경우: 화면의 회사 코드로 레이아웃 조회
|
||||
if (isSuperAdmin) {
|
||||
// 1. 화면 정의의 회사 코드로 레이아웃 조회
|
||||
// 1. 화면 정의의 회사 코드 + 기본 레이어
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
|
||||
[screenId, existingScreen.company_code],
|
||||
);
|
||||
|
||||
// 2. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째 조회
|
||||
// 2. 기본 레이어 없으면 layer_id 조건 없이 조회 (하위 호환)
|
||||
if (!layout) {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id ASC
|
||||
LIMIT 1`,
|
||||
[screenId, existingScreen.company_code],
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째
|
||||
if (!layout) {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
ORDER BY layer_id ASC
|
||||
LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 일반 사용자: 기존 로직 (회사별 우선, 없으면 공통(*) 조회)
|
||||
// 일반 사용자: 회사별 우선 + 기본 레이어
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
// 회사별 기본 레이어 없으면 layer_id 조건 없이 (하위 호환)
|
||||
if (!layout) {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id ASC
|
||||
LIMIT 1`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
// 회사별 레이아웃이 없으면 공통(*) 레이아웃 조회
|
||||
if (!layout && companyCode !== "*") {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = '*'`,
|
||||
WHERE screen_id = $1 AND company_code = '*'
|
||||
ORDER BY layer_id ASC
|
||||
LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
@@ -5122,17 +5147,21 @@ export class ScreenManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 레이아웃 저장 (1 레코드 방식)
|
||||
* - screen_layouts_v2 테이블에 화면당 1개 레코드 저장
|
||||
* - layout_data JSON에 모든 컴포넌트 포함
|
||||
* V2 레이아웃 저장 (레이어별 저장)
|
||||
* - screen_layouts_v2 테이블에 화면당 레이어별 1개 레코드 저장
|
||||
* - layout_data JSON에 해당 레이어의 컴포넌트 포함
|
||||
*/
|
||||
async saveLayoutV2(
|
||||
screenId: number,
|
||||
layoutData: any,
|
||||
companyCode: string,
|
||||
): Promise<void> {
|
||||
const layerId = layoutData.layerId || 1;
|
||||
const layerName = layoutData.layerName || (layerId === 1 ? '기본 레이어' : `레이어 ${layerId}`);
|
||||
const conditionConfig = layoutData.conditionConfig || null;
|
||||
|
||||
console.log(`=== V2 레이아웃 저장 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 레이어: ${layerId} (${layerName})`);
|
||||
console.log(`컴포넌트 수: ${layoutData.components?.length || 0}`);
|
||||
|
||||
// 권한 확인
|
||||
@@ -5151,22 +5180,173 @@ export class ScreenManagementService {
|
||||
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// 버전 정보 추가 (updatedAt은 DB 컬럼 updated_at으로 관리)
|
||||
// 저장할 layout_data에서 레이어 메타 정보 제거 (순수 레이아웃만 저장)
|
||||
const { layerId: _lid, layerName: _ln, conditionConfig: _cc, ...pureLayoutData } = layoutData;
|
||||
const dataToSave = {
|
||||
version: "2.0",
|
||||
...layoutData
|
||||
...pureLayoutData,
|
||||
};
|
||||
|
||||
// UPSERT (있으면 업데이트, 없으면 삽입)
|
||||
// UPSERT (레이어별 저장)
|
||||
await query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW()`,
|
||||
[screenId, companyCode, JSON.stringify(dataToSave)],
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, condition_config, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id)
|
||||
DO UPDATE SET layout_data = $6, layer_name = $4, condition_config = $5, updated_at = NOW()`,
|
||||
[screenId, companyCode, layerId, layerName, conditionConfig ? JSON.stringify(conditionConfig) : null, JSON.stringify(dataToSave)],
|
||||
);
|
||||
|
||||
console.log(`V2 레이아웃 저장 완료`);
|
||||
console.log(`V2 레이아웃 저장 완료 (레이어 ${layerId})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면의 모든 레이어 목록 조회
|
||||
* 레이어가 없으면 기본 레이어를 자동 생성
|
||||
*/
|
||||
async getScreenLayers(
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
): Promise<any[]> {
|
||||
let layers;
|
||||
|
||||
if (companyCode === "*") {
|
||||
layers = await query<any>(
|
||||
`SELECT layer_id, layer_name, condition_config,
|
||||
jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
|
||||
updated_at
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1
|
||||
ORDER BY layer_id`,
|
||||
[screenId],
|
||||
);
|
||||
} else {
|
||||
layers = await query<any>(
|
||||
`SELECT layer_id, layer_name, condition_config,
|
||||
jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
|
||||
updated_at
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
// 회사별 레이어가 없으면 공통(*) 레이어 조회
|
||||
if (layers.length === 0 && companyCode !== "*") {
|
||||
layers = await query<any>(
|
||||
`SELECT layer_id, layer_name, condition_config,
|
||||
jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
|
||||
updated_at
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = '*'
|
||||
ORDER BY layer_id`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 레이어가 없으면 기본 레이어 자동 생성
|
||||
if (layers.length === 0) {
|
||||
const defaultLayout = JSON.stringify({ version: "2.0", components: [] });
|
||||
await query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, 1, '기본 레이어', $3, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id) DO NOTHING`,
|
||||
[screenId, companyCode, defaultLayout],
|
||||
);
|
||||
console.log(`기본 레이어 자동 생성: screen_id=${screenId}, company_code=${companyCode}`);
|
||||
|
||||
// 다시 조회
|
||||
layers = await query<any>(
|
||||
`SELECT layer_id, layer_name, condition_config,
|
||||
jsonb_array_length(COALESCE(layout_data->'components', '[]'::jsonb)) as component_count,
|
||||
updated_at
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 레이어의 레이아웃 조회
|
||||
*/
|
||||
async getLayerLayout(
|
||||
screenId: number,
|
||||
layerId: number,
|
||||
companyCode: string,
|
||||
): Promise<any> {
|
||||
let layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>(
|
||||
`SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
|
||||
[screenId, companyCode, layerId],
|
||||
);
|
||||
|
||||
// 회사별 레이어가 없으면 공통(*) 조회
|
||||
if (!layout && companyCode !== "*") {
|
||||
layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>(
|
||||
`SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = '*' AND layer_id = $2`,
|
||||
[screenId, layerId],
|
||||
);
|
||||
}
|
||||
|
||||
if (!layout) return null;
|
||||
|
||||
return {
|
||||
...layout.layout_data,
|
||||
layerId,
|
||||
layerName: layout.layer_name,
|
||||
conditionConfig: layout.condition_config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이어 삭제
|
||||
*/
|
||||
async deleteLayer(
|
||||
screenId: number,
|
||||
layerId: number,
|
||||
companyCode: string,
|
||||
): Promise<void> {
|
||||
if (layerId === 1) {
|
||||
throw new Error("기본 레이어는 삭제할 수 없습니다.");
|
||||
}
|
||||
|
||||
await query(
|
||||
`DELETE FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
|
||||
[screenId, companyCode, layerId],
|
||||
);
|
||||
|
||||
console.log(`레이어 삭제 완료: screen_id=${screenId}, layer_id=${layerId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이어 조건 설정 업데이트
|
||||
*/
|
||||
async updateLayerCondition(
|
||||
screenId: number,
|
||||
layerId: number,
|
||||
companyCode: string,
|
||||
conditionConfig: any,
|
||||
layerName?: string,
|
||||
): Promise<void> {
|
||||
const setClauses = ['condition_config = $4', 'updated_at = NOW()'];
|
||||
const params: any[] = [screenId, companyCode, layerId, conditionConfig ? JSON.stringify(conditionConfig) : null];
|
||||
|
||||
if (layerName) {
|
||||
setClauses.push(`layer_name = $${params.length + 1}`);
|
||||
params.push(layerName);
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE screen_layouts_v2 SET ${setClauses.join(', ')}
|
||||
WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user