feat: POP 회사별 자동 세팅 — 메뉴 활성화 시 레이아웃 자동 생성

- 로그인 시 POP 메뉴 발견 → 해당 회사용 POP 레이아웃 8개 자동 복제
- 템플릿: 공통(*) 우선, COMPANY_7 폴백
- 회사명 자동 치환 (탑씰 → 해당 회사명)
- screen_definitions 공통(*) 화면은 모든 회사 접근 허용
- 프로필 POP 모드 메뉴: POP 메뉴 있는 회사만 표시
- getLayoutPop 개별 자동 복제 (2중 안전망)
This commit is contained in:
SeongHyun Kim
2026-04-07 10:46:54 +09:00
parent 15029dc58d
commit 21e89922eb
3 changed files with 176 additions and 19 deletions

View File

@@ -7,6 +7,7 @@ import { JwtUtils } from "../utils/jwtUtils";
import { LoginRequest, UserInfo, ApiResponse, PersonBean } from "../types/auth";
import { logger } from "../utils/logger";
import { sendSmartFactoryLog } from "../utils/smartFactoryLog";
import { query, queryOne } from "../database/db";
export class AuthController {
/**
@@ -104,6 +105,14 @@ export class AuthController {
popLandingPath = "/pop";
}
logger.debug(`POP 랜딩 경로: ${popLandingPath}`);
// POP 메뉴가 존재하면 해당 회사의 POP 레이아웃 자동 초기화 (비동기)
if (popLandingPath) {
const companyCode = loginResult.userInfo.companyCode || "ILSHIN";
AuthController.initPopLayoutsForCompany(companyCode).catch((err) => {
logger.warn("POP 레이아웃 자동 초기화 중 오류 (무시):", err);
});
}
} catch (popError) {
logger.warn("POP 메뉴 조회 중 오류 (무시):", popError);
}
@@ -562,4 +571,78 @@ export class AuthController {
});
}
}
/**
* POP 레이아웃 자동 초기화
* 해당 회사의 screen_layouts_pop 레코드가 없으면
* 템플릿(공통 '*' 또는 COMPANY_7)에서 복제하여 생성
*
* 기본 POP 화면 ID: 5, 6, 7, 8, 6526, 6527, 6528, 6529
*/
static async initPopLayoutsForCompany(companyCode: string): Promise<void> {
// SUPER_ADMIN이나 공통(*)은 초기화 불필요
if (companyCode === "*" || companyCode === "COMPANY_7") return;
const POP_SCREEN_IDS = [5, 6, 7, 8, 6526, 6527, 6528, 6529];
// 이미 해당 회사의 POP 레이아웃이 하나라도 있으면 스킵 (중복 초기화 방지)
const existing = await query<{ cnt: string }>(
`SELECT COUNT(*)::text AS cnt FROM screen_layouts_pop
WHERE company_code = $1 AND screen_id = ANY($2::int[])`,
[companyCode, POP_SCREEN_IDS],
);
const existingCount = parseInt(existing[0]?.cnt || "0", 10);
if (existingCount > 0) {
logger.debug(`POP 레이아웃 이미 존재 (${companyCode}): ${existingCount}개, 스킵`);
return;
}
logger.info(`POP 레이아웃 자동 초기화 시작: ${companyCode}`);
// 회사명 조회 (레이아웃 내 회사명 치환용)
const companyInfo = await queryOne<{ company_name: string }>(
`SELECT company_name FROM company_mng WHERE company_code = $1`,
[companyCode],
);
const companyName = companyInfo?.company_name || companyCode;
let initCount = 0;
for (const screenId of POP_SCREEN_IDS) {
// 템플릿 조회: 공통(*) 우선, 없으면 COMPANY_7 폴백
let template = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = '*'`,
[screenId],
);
if (!template) {
template = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = 'COMPANY_7'`,
[screenId],
);
}
if (!template) {
logger.debug(`POP 템플릿 없음 (screen_id=${screenId}), 스킵`);
continue;
}
// 레이아웃 복제 + 회사명 치환
const layoutStr = JSON.stringify(template.layout_data);
const replacedStr = layoutStr
.replace(/\(주\)탑씰/g, companyName)
.replace(/탑씰/g, companyName)
.replace(/TOPSEAL/gi, companyName);
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(), 'SYSTEM', 'SYSTEM')
ON CONFLICT (screen_id, company_code) DO NOTHING`,
[screenId, companyCode, replacedStr],
);
initCount++;
}
logger.info(`POP 레이아웃 자동 초기화 완료: ${companyCode}, ${initCount}개 화면`);
}
}

View File

@@ -5909,7 +5909,8 @@ export class ScreenManagementService {
const existingScreen = screens[0];
// SUPER_ADMIN이 아니고 회사 코드가 다르면 권한 없음
if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode) {
// screen_definitions.company_code가 '*'(공통 화면)이면 모든 회사에서 접근 허용
if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode && existingScreen.company_code !== '*') {
throw new Error("이 화면의 POP 레이아웃을 조회할 권한이 없습니다.");
}
@@ -5935,20 +5936,64 @@ export class ScreenManagementService {
);
}
} 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 }>(
// 1. 공통(*) 템플릿 조회
let templateLayout = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = '*'`,
[screenId],
);
// 2. 공통 없으면 COMPANY_7(탑씰) 폴백
if (!templateLayout) {
templateLayout = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = 'COMPANY_7'`,
[screenId],
);
}
// 3. 템플릿이 있으면 해당 회사용으로 복제
if (templateLayout) {
console.log(`POP 레이아웃 자동 복제: screen_id=${screenId}, 대상 회사=${companyCode}`);
// 회사명 조회 (레이아웃 내 회사명 치환용)
const companyInfo = await queryOne<{ company_name: string }>(
`SELECT company_name FROM company_mng WHERE company_code = $1`,
[companyCode],
);
const companyName = companyInfo?.company_name || companyCode;
let clonedData = JSON.parse(JSON.stringify(templateLayout.layout_data));
// layout_data 내 회사명 텍스트 치환 (탑씰 관련 문자열 → 대상 회사명)
const layoutStr = JSON.stringify(clonedData);
const replacedStr = layoutStr
.replace(/\(주\)탑씰/g, companyName)
.replace(/탑씰/g, companyName)
.replace(/TOPSEAL/gi, companyName);
clonedData = JSON.parse(replacedStr);
// 해당 회사 코드로 INSERT (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(), 'SYSTEM', 'SYSTEM')
ON CONFLICT (screen_id, company_code)
DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = 'SYSTEM'`,
[screenId, companyCode, JSON.stringify(clonedData)],
);
console.log(`POP 레이아웃 자동 복제 완료: screen_id=${screenId}, company=${companyCode}`);
layout = { layout_data: clonedData };
}
}
}
@@ -6041,13 +6086,15 @@ export class ScreenManagementService {
const existingScreen = screens[0];
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
// screen_definitions.company_code가 '*'(공통 화면)이면 모든 회사에서 저장 허용
if (companyCode !== "*" && existingScreen.company_code !== companyCode && existingScreen.company_code !== '*') {
throw new Error("이 화면의 POP 레이아웃을 저장할 권한이 없습니다.");
}
// SUPER_ADMIN인 경우: 화면 정의의 company_code로 저장 (로드와 동일하게)
const targetCompanyCode = companyCode === "*"
? (existingScreen.company_code || "*")
// SUPER_ADMIN인 경우: 화면 정의의 company_code로 저장
// 공통 화면(*)인 경우: 일반 사용자는 자기 회사 코드로 저장 (회사별 레이아웃 분리)
const targetCompanyCode = companyCode === "*"
? (existingScreen.company_code || "*")
: companyCode;
console.log(`저장 대상 company_code: ${targetCompanyCode} (사용자: ${companyCode}, 화면: ${existingScreen.company_code})`);
@@ -6086,10 +6133,11 @@ export class ScreenManagementService {
[],
);
} else {
// 일반 회사: 해당 회사 레이아웃만 조회 (company_code='*'는 최고관리자 전용)
// 일반 회사: 해당 회사 레이아웃 + 공통(*)/COMPANY_7 템플릿도 포함
// (getLayoutPop에서 자동 복제하므로 템플릿이 있으면 해당 회사도 사용 가능)
result = await query<{ screen_id: number }>(
`SELECT DISTINCT screen_id FROM screen_layouts_pop
WHERE company_code = $1`,
WHERE company_code IN ($1, '*', 'COMPANY_7')`,
[companyCode],
);
}
@@ -6121,7 +6169,8 @@ export class ScreenManagementService {
const existingScreen = screens[0];
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
// screen_definitions.company_code가 '*'(공통 화면)이면 모든 회사에서 삭제 허용
if (companyCode !== "*" && existingScreen.company_code !== companyCode && existingScreen.company_code !== '*') {
throw new Error("이 화면의 POP 레이아웃을 삭제할 권한이 없습니다.");
}