Merge branch 'main' into feature/screen-management
This commit is contained in:
@@ -341,6 +341,64 @@ export const uploadFiles = async (
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 레코드 모드: 해당 행의 attachments 컬럼 자동 업데이트
|
||||
const isRecordMode = req.body.isRecordMode === "true" || req.body.isRecordMode === true;
|
||||
|
||||
// 🔍 디버깅: 레코드 모드 조건 확인
|
||||
console.log("🔍 [파일 업로드] 레코드 모드 조건 확인:", {
|
||||
isRecordMode,
|
||||
linkedTable,
|
||||
recordId,
|
||||
columnName,
|
||||
finalTargetObjid,
|
||||
"req.body.isRecordMode": req.body.isRecordMode,
|
||||
"req.body.linkedTable": req.body.linkedTable,
|
||||
"req.body.recordId": req.body.recordId,
|
||||
"req.body.columnName": req.body.columnName,
|
||||
});
|
||||
|
||||
if (isRecordMode && linkedTable && recordId && columnName) {
|
||||
try {
|
||||
// 해당 레코드의 모든 첨부파일 조회
|
||||
const allFiles = await query<any>(
|
||||
`SELECT objid, real_file_name, file_size, file_ext, file_path, regdate
|
||||
FROM attach_file_info
|
||||
WHERE target_objid = $1 AND status = 'ACTIVE'
|
||||
ORDER BY regdate DESC`,
|
||||
[finalTargetObjid]
|
||||
);
|
||||
|
||||
// attachments JSONB 형태로 변환
|
||||
const attachmentsJson = allFiles.map((f: any) => ({
|
||||
objid: f.objid.toString(),
|
||||
realFileName: f.real_file_name,
|
||||
fileSize: Number(f.file_size),
|
||||
fileExt: f.file_ext,
|
||||
filePath: f.file_path,
|
||||
regdate: f.regdate?.toISOString(),
|
||||
}));
|
||||
|
||||
// 해당 테이블의 attachments 컬럼 업데이트
|
||||
// 🔒 멀티테넌시: company_code 필터 추가
|
||||
await query(
|
||||
`UPDATE ${linkedTable}
|
||||
SET ${columnName} = $1::jsonb, updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[JSON.stringify(attachmentsJson), recordId, companyCode]
|
||||
);
|
||||
|
||||
console.log("📎 [레코드 모드] attachments 컬럼 자동 업데이트:", {
|
||||
tableName: linkedTable,
|
||||
recordId: recordId,
|
||||
columnName: columnName,
|
||||
fileCount: attachmentsJson.length,
|
||||
});
|
||||
} catch (updateError) {
|
||||
// attachments 컬럼 업데이트 실패해도 파일 업로드는 성공으로 처리
|
||||
console.warn("⚠️ attachments 컬럼 업데이트 실패 (무시):", updateError);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${files.length}개 파일 업로드 완료`,
|
||||
@@ -405,6 +463,56 @@ export const deleteFile = async (
|
||||
["DELETED", parseInt(objid)]
|
||||
);
|
||||
|
||||
// 🆕 레코드 모드: 해당 행의 attachments 컬럼 자동 업데이트
|
||||
const targetObjid = fileRecord.target_objid;
|
||||
if (targetObjid && !targetObjid.startsWith('screen_files:') && !targetObjid.startsWith('temp_')) {
|
||||
// targetObjid 파싱: tableName:recordId:columnName 형식
|
||||
const parts = targetObjid.split(':');
|
||||
if (parts.length >= 3) {
|
||||
const [tableName, recordId, columnName] = parts;
|
||||
|
||||
try {
|
||||
// 해당 레코드의 남은 첨부파일 조회
|
||||
const remainingFiles = await query<any>(
|
||||
`SELECT objid, real_file_name, file_size, file_ext, file_path, regdate
|
||||
FROM attach_file_info
|
||||
WHERE target_objid = $1 AND status = 'ACTIVE'
|
||||
ORDER BY regdate DESC`,
|
||||
[targetObjid]
|
||||
);
|
||||
|
||||
// attachments JSONB 형태로 변환
|
||||
const attachmentsJson = remainingFiles.map((f: any) => ({
|
||||
objid: f.objid.toString(),
|
||||
realFileName: f.real_file_name,
|
||||
fileSize: Number(f.file_size),
|
||||
fileExt: f.file_ext,
|
||||
filePath: f.file_path,
|
||||
regdate: f.regdate?.toISOString(),
|
||||
}));
|
||||
|
||||
// 해당 테이블의 attachments 컬럼 업데이트
|
||||
// 🔒 멀티테넌시: company_code 필터 추가
|
||||
await query(
|
||||
`UPDATE ${tableName}
|
||||
SET ${columnName} = $1::jsonb, updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[JSON.stringify(attachmentsJson), recordId, fileRecord.company_code]
|
||||
);
|
||||
|
||||
console.log("📎 [파일 삭제] attachments 컬럼 자동 업데이트:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
remainingFiles: attachmentsJson.length,
|
||||
});
|
||||
} catch (updateError) {
|
||||
// attachments 컬럼 업데이트 실패해도 파일 삭제는 성공으로 처리
|
||||
console.warn("⚠️ attachments 컬럼 업데이트 실패 (무시):", updateError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "파일이 삭제되었습니다.",
|
||||
|
||||
@@ -2141,3 +2141,4 @@ export async function multiTableSave(
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,21 @@ export class AdminService {
|
||||
|
||||
// menuType에 따른 WHERE 조건 생성
|
||||
const menuTypeCondition =
|
||||
menuType !== undefined ? `MENU.MENU_TYPE = ${parseInt(menuType)}` : "1 = 1";
|
||||
|
||||
menuType !== undefined
|
||||
? `MENU.MENU_TYPE = ${parseInt(menuType)}`
|
||||
: "1 = 1";
|
||||
|
||||
// 메뉴 관리 화면인지 좌측 사이드바인지 구분
|
||||
// includeInactive가 true거나 menuType이 undefined면 메뉴 관리 화면
|
||||
const includeInactive = paramMap.includeInactive === true;
|
||||
const isManagementScreen = includeInactive || menuType === undefined;
|
||||
// 메뉴 관리 화면: 모든 상태의 메뉴 표시, 좌측 사이드바: active만 표시
|
||||
const statusCondition = isManagementScreen ? "1 = 1" : "MENU.STATUS = 'active'";
|
||||
const subStatusCondition = isManagementScreen ? "1 = 1" : "MENU_SUB.STATUS = 'active'";
|
||||
const statusCondition = isManagementScreen
|
||||
? "1 = 1"
|
||||
: "MENU.STATUS = 'active'";
|
||||
const subStatusCondition = isManagementScreen
|
||||
? "1 = 1"
|
||||
: "MENU_SUB.STATUS = 'active'";
|
||||
|
||||
// 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만)
|
||||
let authFilter = "";
|
||||
@@ -35,7 +41,11 @@ export class AdminService {
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (menuType !== undefined && userType !== "SUPER_ADMIN" && !isManagementScreen) {
|
||||
if (
|
||||
menuType !== undefined &&
|
||||
userType !== "SUPER_ADMIN" &&
|
||||
!isManagementScreen
|
||||
) {
|
||||
// 좌측 사이드바 + SUPER_ADMIN이 아닌 경우만 권한 체크
|
||||
const userRoleGroups = await query<any>(
|
||||
`
|
||||
@@ -56,45 +66,45 @@ export class AdminService {
|
||||
);
|
||||
|
||||
if (userType === "COMPANY_ADMIN") {
|
||||
// 회사 관리자: 자기 회사 메뉴는 모두, 공통 메뉴는 권한 있는 것만
|
||||
// 회사 관리자: 권한 그룹 기반 필터링 적용
|
||||
if (userRoleGroups.length > 0) {
|
||||
const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid);
|
||||
// 루트 메뉴: 회사 코드만 체크 (권한 체크 X)
|
||||
authFilter = `AND MENU.COMPANY_CODE IN ($${paramIndex}, '*')`;
|
||||
// 회사 관리자도 권한 그룹 설정에 따라 메뉴 필터링
|
||||
authFilter = `
|
||||
AND MENU.COMPANY_CODE IN ($${paramIndex}, '*')
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex + 1})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
queryParams.push(userCompanyCode);
|
||||
const companyParamIndex = paramIndex;
|
||||
paramIndex++;
|
||||
|
||||
// 하위 메뉴: 회사 메뉴는 모두, 공통 메뉴는 권한 체크
|
||||
// 하위 메뉴도 권한 체크
|
||||
unionFilter = `
|
||||
AND (
|
||||
MENU_SUB.COMPANY_CODE = $${companyParamIndex}
|
||||
OR (
|
||||
MENU_SUB.COMPANY_CODE = '*'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
)
|
||||
AND MENU_SUB.COMPANY_CODE IN ($${paramIndex - 1}, '*')
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
queryParams.push(roleObjids);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 회사 관리자: 회사 ${userCompanyCode} 메뉴 전체 + 권한 있는 공통 메뉴`
|
||||
`✅ 회사 관리자: 권한 있는 메뉴만 (${roleObjids.length}개 그룹)`
|
||||
);
|
||||
} else {
|
||||
// 권한 그룹이 없는 회사 관리자: 자기 회사 메뉴만
|
||||
authFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`;
|
||||
unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex}`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 회사 관리자 (권한 그룹 없음): 회사 ${userCompanyCode} 메뉴만`
|
||||
// 권한 그룹이 없는 회사 관리자: 메뉴 없음
|
||||
logger.warn(
|
||||
`⚠️ 회사 관리자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
// 일반 사용자: 권한 그룹 필수
|
||||
@@ -131,7 +141,11 @@ export class AdminService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} else if (menuType !== undefined && userType === "SUPER_ADMIN" && !isManagementScreen) {
|
||||
} else if (
|
||||
menuType !== undefined &&
|
||||
userType === "SUPER_ADMIN" &&
|
||||
!isManagementScreen
|
||||
) {
|
||||
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
|
||||
logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
|
||||
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
|
||||
@@ -167,7 +181,7 @@ export class AdminService {
|
||||
companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
|
||||
|
||||
// 하위 메뉴에도 회사 필터링 적용 (공통 메뉴 제외)
|
||||
if (unionFilter === "") {
|
||||
unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex - 1}`;
|
||||
|
||||
@@ -903,6 +903,9 @@ export class DynamicFormService {
|
||||
return `${key} = $${index + 1}::numeric`;
|
||||
} else if (dataType === "boolean") {
|
||||
return `${key} = $${index + 1}::boolean`;
|
||||
} else if (dataType === 'jsonb' || dataType === 'json') {
|
||||
// 🆕 JSONB/JSON 타입은 명시적 캐스팅
|
||||
return `${key} = $${index + 1}::jsonb`;
|
||||
} else {
|
||||
// 문자열 타입은 캐스팅 불필요
|
||||
return `${key} = $${index + 1}`;
|
||||
@@ -910,7 +913,17 @@ export class DynamicFormService {
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
const values: any[] = Object.values(changedFields);
|
||||
// 🆕 JSONB 타입 값은 JSON 문자열로 변환
|
||||
const values: any[] = Object.keys(changedFields).map((key) => {
|
||||
const value = changedFields[key];
|
||||
const dataType = columnTypes[key];
|
||||
|
||||
// JSONB/JSON 타입이고 배열/객체인 경우 JSON 문자열로 변환
|
||||
if ((dataType === 'jsonb' || dataType === 'json') && (Array.isArray(value) || (typeof value === 'object' && value !== null))) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
values.push(id); // WHERE 조건용 ID 추가
|
||||
|
||||
// 🔑 Primary Key 타입에 맞게 캐스팅
|
||||
|
||||
@@ -607,7 +607,9 @@ class NumberingRuleService {
|
||||
}
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
if (result.rowCount === 0) return null;
|
||||
if (result.rowCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rule = result.rows[0];
|
||||
|
||||
|
||||
@@ -2360,30 +2360,33 @@ export class ScreenManagementService {
|
||||
const lockId = Buffer.from(companyCode).reduce((acc, byte) => acc + byte, 0);
|
||||
await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]);
|
||||
|
||||
// 현재 최대 번호 조회
|
||||
const existingScreens = await client.query<{ screen_code: string }>(
|
||||
`SELECT screen_code FROM screen_definitions
|
||||
WHERE company_code = $1 AND screen_code LIKE $2
|
||||
ORDER BY screen_code DESC
|
||||
LIMIT 10`,
|
||||
[companyCode, `${companyCode}%`]
|
||||
// 현재 최대 번호 조회 (숫자 추출 후 정렬)
|
||||
// 패턴: COMPANY_CODE_XXX 또는 COMPANY_CODEXXX
|
||||
const existingScreens = await client.query<{ screen_code: string; num: number }>(
|
||||
`SELECT screen_code,
|
||||
COALESCE(
|
||||
NULLIF(
|
||||
regexp_replace(screen_code, $2, '\\1'),
|
||||
screen_code
|
||||
)::integer,
|
||||
0
|
||||
) as num
|
||||
FROM screen_definitions
|
||||
WHERE company_code = $1
|
||||
AND screen_code ~ $2
|
||||
AND deleted_date IS NULL
|
||||
ORDER BY num DESC
|
||||
LIMIT 1`,
|
||||
[companyCode, `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[_]?(\\d+)$`]
|
||||
);
|
||||
|
||||
let maxNumber = 0;
|
||||
const pattern = new RegExp(
|
||||
`^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$`
|
||||
);
|
||||
|
||||
for (const screen of existingScreens.rows) {
|
||||
const match = screen.screen_code.match(pattern);
|
||||
if (match) {
|
||||
const number = parseInt(match[1], 10);
|
||||
if (number > maxNumber) {
|
||||
maxNumber = number;
|
||||
}
|
||||
}
|
||||
if (existingScreens.rows.length > 0 && existingScreens.rows[0].num) {
|
||||
maxNumber = existingScreens.rows[0].num;
|
||||
}
|
||||
|
||||
console.log(`🔢 현재 최대 화면 코드 번호: ${companyCode} → ${maxNumber}`);
|
||||
|
||||
// count개의 코드를 순차적으로 생성
|
||||
const codes: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
||||
Reference in New Issue
Block a user