Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-11-04 15:16:48 +09:00
42 changed files with 5636 additions and 831 deletions

View File

@@ -8,6 +8,7 @@ import config from "../config/environment";
import { AdminService } from "../services/adminService";
import { EncryptUtil } from "../utils/encryptUtil";
import { FileSystemManager } from "../utils/fileSystemManager";
import { validateBusinessNumber } from "../utils/businessNumberValidator";
/**
* 관리자 메뉴 목록 조회
@@ -609,9 +610,15 @@ export const getCompanyList = async (
// Raw Query로 회사 목록 조회
const companies = await query<any>(
`SELECT
company_code,
` SELECT
company_code,
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
status,
writer,
regdate
@@ -1659,9 +1666,15 @@ export async function getCompanyListFromDB(
// Raw Query로 회사 목록 조회
const companies = await query<any>(
`SELECT
company_code,
` SELECT
company_code,
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
writer,
regdate,
status
@@ -2440,6 +2453,25 @@ export const createCompany = async (
[company_name.trim()]
);
// 사업자등록번호 유효성 검증
const businessNumberValidation = validateBusinessNumber(
req.body.business_registration_number?.trim() || ""
);
if (!businessNumberValidation.isValid) {
res.status(400).json({
success: false,
message: businessNumberValidation.message,
errorCode: "INVALID_BUSINESS_NUMBER",
});
return;
}
// Raw Query로 사업자등록번호 중복 체크
const existingBusinessNumber = await queryOne<any>(
`SELECT company_code FROM company_mng WHERE business_registration_number = $1`,
[req.body.business_registration_number?.trim()]
);
if (existingCompany) {
res.status(400).json({
success: false,
@@ -2449,6 +2481,15 @@ export const createCompany = async (
return;
}
if (existingBusinessNumber) {
res.status(400).json({
success: false,
message: "이미 등록된 사업자등록번호입니다.",
errorCode: "DUPLICATE_BUSINESS_NUMBER",
});
return;
}
// PostgreSQL 클라이언트 생성 (복잡한 코드 생성 쿼리용)
const client = new Client({
connectionString:
@@ -2474,11 +2515,17 @@ export const createCompany = async (
const insertQuery = `
INSERT INTO company_mng (
company_code,
company_name,
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
writer,
regdate,
status
) VALUES ($1, $2, $3, $4, $5)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`;
@@ -2488,6 +2535,12 @@ export const createCompany = async (
const insertValues = [
companyCode,
company_name.trim(),
req.body.business_registration_number?.trim() || null,
req.body.representative_name?.trim() || null,
req.body.representative_phone?.trim() || null,
req.body.email?.trim() || null,
req.body.website?.trim() || null,
req.body.address?.trim() || null,
writer,
new Date(),
"active",
@@ -2552,7 +2605,16 @@ export const updateCompany = async (
): Promise<void> => {
try {
const { companyCode } = req.params;
const { company_name, status } = req.body;
const {
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
status,
} = req.body;
logger.info("회사 정보 수정 요청", {
companyCode,
@@ -2586,13 +2648,61 @@ export const updateCompany = async (
return;
}
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
if (business_registration_number && business_registration_number.trim()) {
// 유효성 검증
const businessNumberValidation = validateBusinessNumber(business_registration_number.trim());
if (!businessNumberValidation.isValid) {
res.status(400).json({
success: false,
message: businessNumberValidation.message,
errorCode: "INVALID_BUSINESS_NUMBER",
});
return;
}
// 중복 체크
const duplicateBusinessNumber = await queryOne<any>(
`SELECT company_code FROM company_mng
WHERE business_registration_number = $1 AND company_code != $2`,
[business_registration_number.trim(), companyCode]
);
if (duplicateBusinessNumber) {
res.status(400).json({
success: false,
message: "이미 등록된 사업자등록번호입니다.",
errorCode: "DUPLICATE_BUSINESS_NUMBER",
});
return;
}
}
// Raw Query로 회사 정보 수정
const result = await query<any>(
`UPDATE company_mng
SET company_name = $1, status = $2
WHERE company_code = $3
SET
company_name = $1,
business_registration_number = $2,
representative_name = $3,
representative_phone = $4,
email = $5,
website = $6,
address = $7,
status = $8
WHERE company_code = $9
RETURNING *`,
[company_name.trim(), status || "active", companyCode]
[
company_name.trim(),
business_registration_number?.trim() || null,
representative_name?.trim() || null,
representative_phone?.trim() || null,
email?.trim() || null,
website?.trim() || null,
address?.trim() || null,
status || "active",
companyCode,
]
);
if (result.length === 0) {

View File

@@ -0,0 +1,534 @@
import { Response } from "express";
import { logger } from "../utils/logger";
import { AuthenticatedRequest } from "../types/auth";
import { ApiResponse } from "../types/common";
import { query, queryOne } from "../database/db";
/**
* 부서 목록 조회 (회사별)
*/
export async function getDepartments(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { companyCode } = req.params;
const userCompanyCode = req.user?.companyCode;
logger.info("부서 목록 조회", { companyCode, userCompanyCode });
// 최고 관리자가 아니면 자신의 회사만 조회 가능
if (userCompanyCode !== "*" && userCompanyCode !== companyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 부서를 조회할 권한이 없습니다.",
});
return;
}
// 부서 목록 조회 (부서원 수 포함)
const departments = await query<any>(`
SELECT
d.dept_code,
d.dept_name,
d.company_code,
d.parent_dept_code,
COUNT(DISTINCT ud.user_id) as member_count
FROM dept_info d
LEFT JOIN user_dept ud ON d.dept_code = ud.dept_code
WHERE d.company_code = $1
GROUP BY d.dept_code, d.dept_name, d.company_code, d.parent_dept_code
ORDER BY d.dept_name
`, [companyCode]);
// 응답 형식 변환
const formattedDepartments = departments.map((dept) => ({
dept_code: dept.dept_code,
dept_name: dept.dept_name,
company_code: dept.company_code,
parent_dept_code: dept.parent_dept_code,
memberCount: parseInt(dept.member_count || "0"),
}));
res.status(200).json({
success: true,
data: formattedDepartments,
});
} catch (error) {
logger.error("부서 목록 조회 실패", error);
res.status(500).json({
success: false,
message: "부서 목록 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 부서 상세 조회
*/
export async function getDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode } = req.params;
const department = await queryOne<any>(`
SELECT
dept_code,
dept_name,
company_code,
parent_dept_code
FROM dept_info
WHERE dept_code = $1
`, [deptCode]);
if (!department) {
res.status(404).json({
success: false,
message: "부서를 찾을 수 없습니다.",
});
return;
}
res.status(200).json({
success: true,
data: department,
});
} catch (error) {
logger.error("부서 상세 조회 실패", error);
res.status(500).json({
success: false,
message: "부서 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 부서 생성
*/
export async function createDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { companyCode } = req.params;
const { dept_name, parent_dept_code } = req.body;
if (!dept_name || !dept_name.trim()) {
res.status(400).json({
success: false,
message: "부서명을 입력해주세요.",
});
return;
}
// 같은 회사 내 중복 부서명 확인
const duplicate = await queryOne<any>(`
SELECT dept_code, dept_name
FROM dept_info
WHERE company_code = $1 AND dept_name = $2
`, [companyCode, dept_name.trim()]);
if (duplicate) {
res.status(409).json({
success: false,
message: `"${dept_name}" 부서가 이미 존재합니다.`,
isDuplicate: true,
});
return;
}
// 회사 이름 조회
const company = await queryOne<any>(`
SELECT company_name FROM company_mng WHERE company_code = $1
`, [companyCode]);
const companyName = company?.company_name || companyCode;
// 부서 코드 생성 (전역 카운트: DEPT_1, DEPT_2, ...)
const codeResult = await queryOne<any>(`
SELECT COALESCE(MAX(CAST(SUBSTRING(dept_code FROM 6) AS INTEGER)), 0) + 1 as next_number
FROM dept_info
WHERE dept_code ~ '^DEPT_[0-9]+$'
`);
const nextNumber = codeResult?.next_number || 1;
const deptCode = `DEPT_${nextNumber}`;
// 부서 생성
const result = await query<any>(`
INSERT INTO dept_info (
dept_code,
dept_name,
company_code,
company_name,
parent_dept_code,
status,
regdate
) VALUES ($1, $2, $3, $4, $5, $6, NOW())
RETURNING *
`, [
deptCode,
dept_name.trim(),
companyCode,
companyName,
parent_dept_code || null,
'active',
]);
logger.info("부서 생성 성공", { deptCode, dept_name });
res.status(201).json({
success: true,
message: "부서가 생성되었습니다.",
data: result[0],
});
} catch (error) {
logger.error("부서 생성 실패", error);
res.status(500).json({
success: false,
message: "부서 생성 중 오류가 발생했습니다.",
});
}
}
/**
* 부서 수정
*/
export async function updateDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode } = req.params;
const { dept_name, parent_dept_code } = req.body;
if (!dept_name || !dept_name.trim()) {
res.status(400).json({
success: false,
message: "부서명을 입력해주세요.",
});
return;
}
const result = await query<any>(`
UPDATE dept_info
SET
dept_name = $1,
parent_dept_code = $2
WHERE dept_code = $3
RETURNING *
`, [dept_name.trim(), parent_dept_code || null, deptCode]);
if (result.length === 0) {
res.status(404).json({
success: false,
message: "부서를 찾을 수 없습니다.",
});
return;
}
logger.info("부서 수정 성공", { deptCode });
res.status(200).json({
success: true,
message: "부서가 수정되었습니다.",
data: result[0],
});
} catch (error) {
logger.error("부서 수정 실패", error);
res.status(500).json({
success: false,
message: "부서 수정 중 오류가 발생했습니다.",
});
}
}
/**
* 부서 삭제
*/
export async function deleteDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode } = req.params;
// 하위 부서 확인
const hasChildren = await queryOne<any>(`
SELECT COUNT(*) as count
FROM dept_info
WHERE parent_dept_code = $1
`, [deptCode]);
if (parseInt(hasChildren?.count || "0") > 0) {
res.status(400).json({
success: false,
message: "하위 부서가 있는 부서는 삭제할 수 없습니다. 먼저 하위 부서를 삭제해주세요.",
});
return;
}
// 부서원 삭제 (부서 삭제 전에 먼저 삭제)
const deletedMembers = await query<any>(`
DELETE FROM user_dept
WHERE dept_code = $1
RETURNING user_id
`, [deptCode]);
const memberCount = deletedMembers.length;
// 부서 삭제
const result = await query<any>(`
DELETE FROM dept_info
WHERE dept_code = $1
RETURNING dept_code, dept_name
`, [deptCode]);
if (result.length === 0) {
res.status(404).json({
success: false,
message: "부서를 찾을 수 없습니다.",
});
return;
}
logger.info("부서 삭제 성공", {
deptCode,
deptName: result[0].dept_name,
deletedMemberCount: memberCount
});
res.status(200).json({
success: true,
message: memberCount > 0
? `부서가 삭제되었습니다. (부서원 ${memberCount}명 제외됨)`
: "부서가 삭제되었습니다.",
});
} catch (error) {
logger.error("부서 삭제 실패", error);
res.status(500).json({
success: false,
message: "부서 삭제 중 오류가 발생했습니다.",
});
}
}
/**
* 부서원 목록 조회
*/
export async function getDepartmentMembers(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode } = req.params;
const members = await query<any>(`
SELECT
u.user_id,
u.user_name,
u.email,
u.tel as phone,
u.cell_phone,
u.position_name,
ud.dept_code,
d.dept_name,
ud.is_primary
FROM user_dept ud
JOIN user_info u ON ud.user_id = u.user_id
JOIN dept_info d ON ud.dept_code = d.dept_code
WHERE ud.dept_code = $1
ORDER BY ud.is_primary DESC, u.user_name
`, [deptCode]);
res.status(200).json({
success: true,
data: members,
});
} catch (error) {
logger.error("부서원 목록 조회 실패", error);
res.status(500).json({
success: false,
message: "부서원 목록 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 사용자 검색 (부서원 추가용)
*/
export async function searchUsers(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { companyCode } = req.params;
const { search } = req.query;
if (!search || typeof search !== 'string') {
res.status(400).json({
success: false,
message: "검색어를 입력해주세요.",
});
return;
}
// 사용자 검색 (ID 또는 이름)
const users = await query<any>(`
SELECT
user_id,
user_name,
email,
position_name,
company_code
FROM user_info
WHERE company_code = $1
AND (
user_id ILIKE $2 OR
user_name ILIKE $2
)
ORDER BY user_name
LIMIT 20
`, [companyCode, `%${search}%`]);
res.status(200).json({
success: true,
data: users,
});
} catch (error) {
logger.error("사용자 검색 실패", error);
res.status(500).json({
success: false,
message: "사용자 검색 중 오류가 발생했습니다.",
});
}
}
/**
* 부서원 추가
*/
export async function addDepartmentMember(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode } = req.params;
const { user_id } = req.body;
if (!user_id) {
res.status(400).json({
success: false,
message: "사용자 ID를 입력해주세요.",
});
return;
}
// 사용자 존재 확인
const user = await queryOne<any>(`
SELECT user_id, user_name
FROM user_info
WHERE user_id = $1
`, [user_id]);
if (!user) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
});
return;
}
// 이미 부서원인지 확인
const existing = await queryOne<any>(`
SELECT *
FROM user_dept
WHERE user_id = $1 AND dept_code = $2
`, [user_id, deptCode]);
if (existing) {
res.status(409).json({
success: false,
message: "이미 해당 부서의 부서원입니다.",
isDuplicate: true,
});
return;
}
// 주 부서가 있는지 확인
const hasPrimary = await queryOne<any>(`
SELECT *
FROM user_dept
WHERE user_id = $1 AND is_primary = true
`, [user_id]);
// 부서원 추가
await query<any>(`
INSERT INTO user_dept (user_id, dept_code, is_primary, created_at)
VALUES ($1, $2, $3, NOW())
`, [user_id, deptCode, !hasPrimary]);
logger.info("부서원 추가 성공", { user_id, deptCode });
res.status(201).json({
success: true,
message: "부서원이 추가되었습니다.",
});
} catch (error) {
logger.error("부서원 추가 실패", error);
res.status(500).json({
success: false,
message: "부서원 추가 중 오류가 발생했습니다.",
});
}
}
/**
* 부서원 제거
*/
export async function removeDepartmentMember(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode, userId } = req.params;
const result = await query<any>(`
DELETE FROM user_dept
WHERE user_id = $1 AND dept_code = $2
RETURNING *
`, [userId, deptCode]);
if (result.length === 0) {
res.status(404).json({
success: false,
message: "해당 부서원을 찾을 수 없습니다.",
});
return;
}
logger.info("부서원 제거 성공", { userId, deptCode });
res.status(200).json({
success: true,
message: "부서원이 제거되었습니다.",
});
} catch (error) {
logger.error("부서원 제거 실패", error);
res.status(500).json({
success: false,
message: "부서원 제거 중 오류가 발생했습니다.",
});
}
}
/**
* 주 부서 설정
*/
export async function setPrimaryDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { deptCode, userId } = req.params;
// 다른 부서의 주 부서 해제
await query<any>(`
UPDATE user_dept
SET is_primary = false
WHERE user_id = $1
`, [userId]);
// 해당 부서를 주 부서로 설정
await query<any>(`
UPDATE user_dept
SET is_primary = true
WHERE user_id = $1 AND dept_code = $2
`, [userId, deptCode]);
logger.info("주 부서 설정 성공", { userId, deptCode });
res.status(200).json({
success: true,
message: "주 부서가 설정되었습니다.",
});
} catch (error) {
logger.error("주 부서 설정 실패", error);
res.status(500).json({
success: false,
message: "주 부서 설정 중 오류가 발생했습니다.",
});
}
}

View File

@@ -12,6 +12,14 @@ export const saveFormData = async (
const { companyCode, userId } = req.user as any;
const { screenId, tableName, data } = req.body;
// 🔍 디버깅: 사용자 정보 확인
console.log("🔍 [saveFormData] 사용자 정보:", {
userId,
companyCode,
reqUser: req.user,
dataWriter: data.writer,
});
// 필수 필드 검증 (screenId는 0일 수 있으므로 undefined 체크)
if (screenId === undefined || screenId === null || !tableName || !data) {
return res.status(400).json({
@@ -25,9 +33,12 @@ export const saveFormData = async (
...data,
created_by: userId,
updated_by: userId,
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
screen_id: screenId,
};
console.log("✅ [saveFormData] 최종 writer 값:", formDataWithMeta.writer);
// company_code는 사용자가 명시적으로 입력한 경우에만 추가
if (data.company_code !== undefined) {
formDataWithMeta.company_code = data.company_code;
@@ -86,6 +97,7 @@ export const saveFormDataEnhanced = async (
...data,
created_by: userId,
updated_by: userId,
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
screen_id: screenId,
};
@@ -134,6 +146,7 @@ export const updateFormData = async (
const formDataWithMeta = {
...data,
updated_by: userId,
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
updated_at: new Date(),
};
@@ -186,6 +199,7 @@ export const updateFormDataPartial = async (
const newDataWithMeta = {
...newData,
updated_by: userId,
writer: newData.writer || userId, // ✅ writer가 없으면 userId로 설정
};
const result = await dynamicFormService.updateFormDataPartial(

View File

@@ -12,6 +12,7 @@ import {
ColumnListResponse,
ColumnSettingsResponse,
} from "../types/tableManagement";
import { query } from "../database/db"; // 🆕 query 함수 import
/**
* 테이블 목록 조회
@@ -506,7 +507,91 @@ export async function updateColumnInputType(
}
/**
* 테이블 데이터 조회 (페이징 + 검색)
* 단일 레코드 조회 (자동 입력용)
*/
export async function getTableRecord(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { filterColumn, filterValue, displayColumn } = req.body;
logger.info(`=== 단일 레코드 조회 시작: ${tableName} ===`);
logger.info(`필터: ${filterColumn} = ${filterValue}`);
logger.info(`표시 컬럼: ${displayColumn}`);
if (!tableName || !filterColumn || !filterValue || !displayColumn) {
const response: ApiResponse<null> = {
success: false,
message: "필수 파라미터가 누락되었습니다.",
error: {
code: "MISSING_PARAMETERS",
details:
"tableName, filterColumn, filterValue, displayColumn이 필요합니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 단일 레코드 조회 (WHERE filterColumn = filterValue)
const result = await tableManagementService.getTableData(tableName, {
page: 1,
size: 1,
search: {
[filterColumn]: filterValue,
},
});
if (!result.data || result.data.length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "데이터를 찾을 수 없습니다.",
error: {
code: "NOT_FOUND",
details: `${filterColumn} = ${filterValue}에 해당하는 데이터가 없습니다.`,
},
};
res.status(404).json(response);
return;
}
const record = result.data[0];
const displayValue = record[displayColumn];
logger.info(`레코드 조회 완료: ${displayColumn} = ${displayValue}`);
const response: ApiResponse<{ value: any; record: any }> = {
success: true,
message: "레코드를 성공적으로 조회했습니다.",
data: {
value: displayValue,
record: record,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("레코드 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "레코드 조회 중 오류가 발생했습니다.",
error: {
code: "RECORD_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* 테이블 데이터 조회 (페이징 + 검색 + 필터링)
*/
export async function getTableData(
req: AuthenticatedRequest,
@@ -520,12 +605,14 @@ export async function getTableData(
search = {},
sortBy,
sortOrder = "asc",
autoFilter, // 🆕 자동 필터 설정 추가 (컴포넌트에서 직접 전달)
} = req.body;
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
logger.info(`페이징: page=${page}, size=${size}`);
logger.info(`검색 조건:`, search);
logger.info(`정렬: ${sortBy} ${sortOrder}`);
logger.info(`자동 필터:`, autoFilter); // 🆕
if (!tableName) {
const response: ApiResponse<null> = {
@@ -542,11 +629,35 @@ export async function getTableData(
const tableManagementService = new TableManagementService();
// 🆕 현재 사용자 필터 적용
let enhancedSearch = { ...search };
if (autoFilter?.enabled && req.user) {
const filterColumn = autoFilter.filterColumn || "company_code";
const userField = autoFilter.userField || "companyCode";
const userValue = (req.user as any)[userField];
if (userValue) {
enhancedSearch[filterColumn] = userValue;
logger.info("🔍 현재 사용자 필터 적용:", {
filterColumn,
userField,
userValue,
tableName,
});
} else {
logger.warn("⚠️ 사용자 정보 필드 값 없음:", {
userField,
user: req.user,
});
}
}
// 데이터 조회
const result = await tableManagementService.getTableData(tableName, {
page: parseInt(page),
size: parseInt(size),
search,
search: enhancedSearch, // 🆕 필터가 적용된 search 사용
sortBy,
sortOrder,
});
@@ -1216,9 +1327,7 @@ export async function getLogData(
originalId: originalId as string,
});
logger.info(
`로그 데이터 조회 완료: ${tableName}_log, ${result.total}`
);
logger.info(`로그 데이터 조회 완료: ${tableName}_log, ${result.total}`);
const response: ApiResponse<typeof result> = {
success: true,
@@ -1254,7 +1363,9 @@ export async function toggleLogTable(
const { tableName } = req.params;
const { isActive } = req.body;
logger.info(`=== 로그 테이블 토글: ${tableName}, isActive: ${isActive} ===`);
logger.info(
`=== 로그 테이블 토글: ${tableName}, isActive: ${isActive} ===`
);
if (!tableName) {
const response: ApiResponse<null> = {
@@ -1288,9 +1399,7 @@ export async function toggleLogTable(
isActive === "Y" || isActive === true
);
logger.info(
`로그 테이블 토글 완료: ${tableName}, isActive: ${isActive}`
);
logger.info(`로그 테이블 토글 완료: ${tableName}, isActive: ${isActive}`);
const response: ApiResponse<null> = {
success: true,