Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -116,12 +116,7 @@ export const addCategoryValue = async (req: AuthenticatedRequest, res: Response)
|
||||
const userId = req.user!.userId;
|
||||
const { menuObjid, ...value } = req.body;
|
||||
|
||||
if (!menuObjid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "menuObjid는 필수입니다",
|
||||
});
|
||||
}
|
||||
// menuObjid는 선택사항 — 옵션설정 등 전역 관리 화면에서는 없을 수 있음
|
||||
|
||||
logger.info("카테고리 값 추가 요청", {
|
||||
tableName: value.tableName,
|
||||
@@ -134,7 +129,7 @@ export const addCategoryValue = async (req: AuthenticatedRequest, res: Response)
|
||||
value,
|
||||
companyCode,
|
||||
userId,
|
||||
Number(menuObjid) // ← menuObjid 전달
|
||||
menuObjid ? Number(menuObjid) : null
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
|
||||
@@ -269,7 +269,7 @@ class TableCategoryValueService {
|
||||
value: TableCategoryValue,
|
||||
companyCode: string,
|
||||
userId: string,
|
||||
menuObjid: number
|
||||
menuObjid: number | null
|
||||
): Promise<TableCategoryValue> {
|
||||
const pool = getPool();
|
||||
|
||||
@@ -286,29 +286,35 @@ class TableCategoryValueService {
|
||||
let duplicateQuery: string;
|
||||
let duplicateParams: any[];
|
||||
|
||||
const menuCondition = menuObjid
|
||||
? "AND menu_objid = $4"
|
||||
: "AND menu_objid IS NULL";
|
||||
const baseParams = menuObjid
|
||||
? [value.tableName, value.columnName, value.valueCode, menuObjid]
|
||||
: [value.tableName, value.columnName, value.valueCode];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 회사에서 중복 체크
|
||||
duplicateQuery = `
|
||||
SELECT value_id
|
||||
SELECT value_id
|
||||
FROM category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND value_code = $3
|
||||
AND menu_objid = $4
|
||||
${menuCondition}
|
||||
`;
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
|
||||
duplicateParams = baseParams;
|
||||
} else {
|
||||
// 일반 회사: 자신의 회사에서만 중복 체크
|
||||
const companyIdx = menuObjid ? "$5" : "$4";
|
||||
duplicateQuery = `
|
||||
SELECT value_id
|
||||
SELECT value_id
|
||||
FROM category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND value_code = $3
|
||||
AND menu_objid = $4
|
||||
AND company_code = $5
|
||||
${menuCondition}
|
||||
AND company_code = ${companyIdx}
|
||||
`;
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
|
||||
duplicateParams = [...baseParams, companyCode];
|
||||
}
|
||||
|
||||
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
||||
@@ -352,11 +358,11 @@ class TableCategoryValueService {
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO category_values (
|
||||
table_name, column_name, value_code, value_label, value_order,
|
||||
value_id, table_name, column_name, value_code, value_label, value_order,
|
||||
parent_value_id, depth, description, color, icon,
|
||||
is_active, is_default, company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING
|
||||
) VALUES ((SELECT COALESCE(MAX(value_id), 0) + 1 FROM category_values), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<Table style={{ minWidth: "900px" }}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[30px]">
|
||||
<TableHead style={{ width: "30px", minWidth: "30px" }}>
|
||||
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead style={{ width: "40px", minWidth: "40px" }} />
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
{ts.visibleColumns.map((col) => (
|
||||
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -43,9 +43,8 @@ function CardListRenderer({ component, getQueryResult }: CardRendererProps) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
const row = queryResult.rows[0];
|
||||
return row[item.fieldName] !== undefined
|
||||
? String(row[item.fieldName])
|
||||
: item.value;
|
||||
const val = row[item.fieldName];
|
||||
return val !== undefined && val !== null ? String(val) : "";
|
||||
}
|
||||
}
|
||||
return item.value;
|
||||
|
||||
@@ -54,9 +54,12 @@ function getGridCellValue(
|
||||
): string {
|
||||
if (cell.cellType === "static") return cell.value ?? "";
|
||||
|
||||
if (cell.cellType === "field" && cell.field && row) {
|
||||
const raw = String(row[cell.field] ?? "");
|
||||
return applyNumberFormat(raw, cell.numberFormat, cell.currencySuffix);
|
||||
if (cell.cellType === "field") {
|
||||
if (cell.field && row) {
|
||||
const raw = String(row[cell.field] ?? "");
|
||||
return applyNumberFormat(raw, cell.numberFormat, cell.currencySuffix);
|
||||
}
|
||||
return ""; // 데이터 없으면 플레이스홀더 숨김
|
||||
}
|
||||
|
||||
return cell.value ?? "";
|
||||
@@ -277,7 +280,7 @@ function ClassicTableRenderer({ component, getQueryResult }: TableRendererProps)
|
||||
color: "#d1d5db",
|
||||
}}
|
||||
>
|
||||
{col.field ? `{${col.field}}` : "—"}
|
||||
{""}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -372,33 +372,35 @@ export function CategoryColumnList({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="shrink-0 space-y-1 px-3 pt-3">
|
||||
<h3 className="text-lg font-semibold">카테고리 컬럼</h3>
|
||||
<p className="text-muted-foreground text-xs">관리할 카테고리 컬럼을 선택하세요</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="컬럼 검색..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-8 pl-8 pr-8 text-xs"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="shrink-0 px-3 pt-3">
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="컬럼 검색..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-8 pl-8 pr-8 text-xs"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto space-y-1 px-3 py-3">
|
||||
{filteredColumns.length === 0 && searchQuery ? (
|
||||
<div className="text-muted-foreground py-4 text-center text-xs">
|
||||
'{searchQuery}'에 대한 검색 결과가 없습니다
|
||||
|
||||
@@ -106,22 +106,13 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
|
||||
const handleAddValue = async (newValue: TableCategoryValue) => {
|
||||
try {
|
||||
if (!menuObjid) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "메뉴 정보가 없습니다. 카테고리 값을 추가할 수 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await addCategoryValue(
|
||||
{
|
||||
...newValue,
|
||||
tableName,
|
||||
columnName,
|
||||
},
|
||||
menuObjid
|
||||
menuObjid || 0
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function getCategoryValues(
|
||||
*/
|
||||
export async function addCategoryValue(
|
||||
value: TableCategoryValue,
|
||||
menuObjid: number
|
||||
menuObjid?: number
|
||||
) {
|
||||
try {
|
||||
const response = await apiClient.post<{
|
||||
@@ -88,7 +88,7 @@ export async function addCategoryValue(
|
||||
data: TableCategoryValue;
|
||||
}>("/table-categories/values", {
|
||||
...value,
|
||||
menuObjid, // ← menuObjid 포함
|
||||
menuObjid: menuObjid || undefined,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user