충돌해결 및 변경사항 커밋

This commit is contained in:
pgb 2025-01-16 17:45:54 +09:00
parent 74f6c9ba83
commit 5e778d3052
5 changed files with 496 additions and 109 deletions

View File

@ -21,6 +21,7 @@ const oemMngController = require("../controllers/app/common/oemmng.controller");
const productgroupController = require("../controllers/app/common/productgroup.controller");
const productController = require("../controllers/app/common/product.controller");
const carMngController = require("../controllers/app/common/carmng.controller");
const contractController = require("../controllers/app/contract/contract.controller");
router.use("/health", healthController);
router.use("/auth", authController);
@ -41,5 +42,6 @@ router.use("/oemmng", oemMngController);
router.use("/productgroup", productgroupController);
router.use("/product", productController);
router.use("/carmng", carMngController);
router.use("/contract", contractController);
module.exports = router;

View File

@ -1,5 +1,5 @@
// src/services/contract.service.js
const { Contract, ContractDetail, Company } = require("../models");
const { Contract, ContractDetail, Company, ContractMgmt} = require("../models");
const alertService = require("./alert.service");
const { Op } = require("sequelize");
// const { getKoreanSubjectParticle } = require("../utils/koreanParticle");
@ -114,6 +114,118 @@ class ContractService {
},
};
}
// 영업정보 목록 조회 수정
async getContractList(page = 1, limit = 20) {
try {
const offset = (page - 1) * limit;
const { count, rows } = await ContractMgmt.findAndCountAll({
attributes: {
exclude: ['createdAt', 'updatedAt', 'created_at', 'updated_at'] // 모든 타임스탬프 필드 제외
},
order: [['created_at', 'DESC']], // created_at으로 수정
limit: limit,
offset: offset,
raw: true
});
if (!rows || !Array.isArray(rows)) {
throw new Error('No data found');
}
// 데이터 형식 변환
const formatted = rows.map(row => ({
...row,
// Date 형식 변환 및 검사 추가
customer_info: row.customer_info ? new Date(row.customer_info).toISOString() : null,
duckil_info: row.duckil_info ? new Date(row.duckil_info).toISOString() : null,
register_date: row.register_date ? new Date(row.register_date).toISOString() : null,
request_date: row.request_date ? new Date(row.request_date).toISOString() : null,
submit_date: row.submit_date ? new Date(row.submit_date).toISOString() : null,
created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
updated_at: row.updated_at ? new Date(row.updated_at).toISOString() : null,
// 숫자 데이터 변환
progress: row.progress ? parseFloat(row.progress) : null,
customer_total: row.customer_total ? parseInt(row.customer_total, 10) : null,
duckil_production: row.duckil_production ? parseInt(row.duckil_production, 10) : null,
first_quarter: row.first_quarter ? parseInt(row.first_quarter, 10) : null,
second_quarter: row.second_quarter ? parseInt(row.second_quarter, 10) : null,
third_quarter: row.third_quarter ? parseInt(row.third_quarter, 10) : null,
fourth_quarter: row.fourth_quarter ? parseInt(row.fourth_quarter, 10) : null,
}));
return {
oemMngs: formatted,
total: count,
totalPages: Math.ceil(count / limit),
currentPage: page,
pageSize: limit
};
} catch (error) {
console.error('Contract Service Error:', error); // 에러 로깅 추가
throw error;
}
}
// 영업정보 생성 (데이터 타입 변환 추가)
async createContractMgmt(contractData) {
try {
const formattedData = this._formatContractData(contractData);
return await ContractMgmt.create(formattedData);
} catch (error) {
throw error;
}
}
// 영업정보 수정 (데이터 타입 변환 추가)
async updateContractMgmt(id, updateData) {
try {
const contract = await ContractMgmt.findByPk(id);
if (!contract) throw new Error('Contract not found');
const formattedData = this._formatContractData(updateData);
await contract.update(formattedData);
return contract;
} catch (error) {
throw error;
}
}
// 영업정보 삭제
async deleteContractMgmt(id) {
try {
const contract = await ContractMgmt.findByPk(id);
if (!contract) throw new Error('Contract not found');
await contract.destroy();
return true;
} catch (error) {
throw error;
}
}
// 데이터 형식 변환 헬퍼 메서드
_formatContractData(data) {
return {
...data,
// 날짜 문자열을 Date 객체로 변환
customer_info: data.customer_info ? new Date(data.customer_info) : null,
duckil_info: data.duckil_info ? new Date(data.duckil_info) : null,
register_date: data.register_date ? new Date(data.register_date) : null,
request_date: data.request_date ? new Date(data.request_date) : null,
submit_date: data.submit_date ? new Date(data.submit_date) : null,
// 숫자 문자열을 숫자로 변환
progress: data.progress ? Number(data.progress) : null,
customer_total: data.customer_total ? Number(data.customer_total) : null,
duckil_production: data.duckil_production ? Number(data.duckil_production) : null,
first_quarter: data.first_quarter ? Number(data.first_quarter) : null,
second_quarter: data.second_quarter ? Number(data.second_quarter) : null,
third_quarter: data.third_quarter ? Number(data.third_quarter) : null,
fourth_quarter: data.fourth_quarter ? Number(data.fourth_quarter) : null,
};
}
}
module.exports = new ContractService();

View File

@ -422,29 +422,6 @@ const CommonCodePage = () => {
<div>{new Date(row.original.createdAt).toLocaleDateString()}</div>
),
},
{
id: "isActive",
accessorKey: "isActive",
header: "활성화",
meta: {
width: "200px",
textAlign: "center"
},
cell: ({ row }) => (
<div className="text-center">
<Switch
checked={row.original.isActive}
onCheckedChange={(checked) =>
toggleActiveMutation.mutate({
id: row.original.id,
isActive: checked
})
}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
},
{
id: "addSub",
header: "하위요소",
@ -468,6 +445,29 @@ const CommonCodePage = () => {
</div>
),
},
{
id: "isActive",
accessorKey: "isActive",
header: "활성화",
meta: {
width: "200px",
textAlign: "center"
},
cell: ({ row }) => (
<div className="text-center">
<Switch
checked={row.original.isActive}
onCheckedChange={(checked) =>
toggleActiveMutation.mutate({
id: row.original.id,
isActive: checked
})
}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
},
{
id: "actions",
header: "삭제",

View File

@ -373,27 +373,6 @@ const MenusPage = () => {
textAlign: "center"
}
},
{
id: "menu_type",
accessorKey: "menu_type",
header: "타입",
meta: {
width: "200px",
textAlign: "center"
},
cell: ({ row }) => (
<div className="text-center">
<span className={cn(
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
row.original.menu_type === "0"
? "bg-blue-50 text-blue-700 ring-blue-700/10"
: "bg-green-50 text-green-700 ring-green-600/20"
)}>
{row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"}
</span>
</div>
),
},
{
id: "addSub",
header: "하위요소",
@ -417,6 +396,27 @@ const MenusPage = () => {
</div>
),
},
{
id: "menu_type",
accessorKey: "menu_type",
header: "타입",
meta: {
width: "200px",
textAlign: "center"
},
cell: ({ row }) => (
<div className="text-center">
<span className={cn(
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
row.original.menu_type === "0"
? "bg-blue-50 text-blue-700 ring-blue-700/10"
: "bg-green-50 text-green-700 ring-green-600/20"
)}>
{row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"}
</span>
</div>
),
},
{
id: "isActive",
accessorKey: "isActive",

View File

@ -13,6 +13,8 @@ import {
type OnChangeFn,
type Row,
type TableOptions,
type GroupingColumnDef,
type Cell,
} from "@tanstack/react-table";
import { ChevronRight, ChevronDown } from "lucide-react";
@ -44,10 +46,79 @@ interface PLMTableProps<T extends BaseRow> {
onCheckedChange?: (checkedRows: T[]) => void;
onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가
isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가
renderExpandedRow?: (row: Row<T>) => React.ReactNode; // 확장된 행 렌더링을 위한 prop
enableExpander?: boolean; // 확장 기능 활성화 여부
treeConfig?: { // 트리 구조 설정
getParentId?: (row: T) => string | null;
indent?: number;
};
expanderConfig?: { // 행 확장 설정
renderExpandedContent?: (row: Row<T>) => React.ReactNode;
expandedContentStyle?: React.CSSProperties;
};
onCellEdit?: (rowId: string, field: string, value: any) => Promise<void>;
editableColumns?: string[]; // 수정 가능한 컬럼 목록
}
type CustomExpandedState = Record<string | number, boolean>;
// EditableCell 컴포넌트 추가
const EditableCell = <T extends BaseRow>({
cell,
isEditable,
onCellEdit,
}: {
cell: Cell<T, unknown>;
isEditable?: boolean;
onCellEdit?: (rowId: string, field: string, value: any) => Promise<void>;
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
const handleDoubleClick = () => {
if (!isEditable) return;
setIsEditing(true);
setEditValue(String(cell.getValue() || ''));
};
const handleSave = async () => {
if (onCellEdit) {
try {
await onCellEdit(cell.row.original.id, String(cell.column.id), editValue);
setIsEditing(false);
} catch (error) {
console.error('Failed to update cell:', error);
}
}
};
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
if (isEditing) {
return (
<input
ref={inputRef}
className="w-full px-2 py-1 border rounded"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={handleSave}
onKeyPress={(e) => e.key === 'Enter' && handleSave()}
/>
);
}
return (
<div onDoubleClick={handleDoubleClick}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
);
};
export function PLMTable<T extends BaseRow>({
columns: userColumns,
data,
@ -57,6 +128,12 @@ export function PLMTable<T extends BaseRow>({
onCheckedChange,
onPageSizeChange,
isTreeTable = false, // 기본값은 false
renderExpandedRow,
enableExpander = false,
treeConfig,
expanderConfig,
onCellEdit,
editableColumns,
}: PLMTableProps<T>) {
const [expanded, setExpanded] = useState<CustomExpandedState>({});
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
@ -218,7 +295,7 @@ export function PLMTable<T extends BaseRow>({
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
className={`h-8 w-8 p-0 ${row.depth > 0 ? 'ml-4' : ''}`}
onClick={(e) => {
e.stopPropagation();
row.toggleExpanded();
@ -236,11 +313,39 @@ export function PLMTable<T extends BaseRow>({
});
}
// 확장/축소 버튼 컬럼 - enableExpander가 true일 때만 추가
if (enableExpander) {
result.push({
id: 'expander',
meta: { width: "40px" },
header: () => null,
cell: ({ row }) => (
<div className="flex justify-center">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
row.toggleExpanded();
}}
>
{row.getIsExpanded() ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</div>
),
});
}
// 나머지 컬럼들 추가
result.push(...userColumns);
return result;
}, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]);
}, [userColumns, enableCheckbox, onCheckedChange, isTreeTable, enableExpander]);
const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => {
setExpanded(old => {
@ -307,77 +412,245 @@ export function PLMTable<T extends BaseRow>({
onPageSizeChange(newPageSize);
}
};
const getHeaderDepth = (columns: GroupingColumnDef<T, any>[]): number => {
let maxDepth = 1;
for (const column of columns) {
if ('columns' in column && Array.isArray(column.columns)) {
const subDepth = getHeaderDepth(column.columns as GroupingColumnDef<T, any>[]) + 1;
maxDepth = Math.max(maxDepth, subDepth);
}
}
return maxDepth;
};
const defaultRenderExpandedRow = (row: Row<T>) => {
const item = row.original;
return (
<div className="pl-12 pr-4 py-2 bg-gray-50">
<table className="min-w-full border-separate border-spacing-0 text-xs">
{Object.entries(item).map(([key, value]) => {
// id나 기본 표시되는 컬럼들은 제외
if (key === 'id' || columns.some(col => 'accessorKey' in col && col.accessorKey === key)) {
return null;
}
return (
<tr key={key}>
<th className="w-48 py-1 px-2 text-left font-medium text-gray-700">{key}</th>
<td className="py-1 px-2">{String(value)}</td>
</tr>
);
})}
</table>
</div>
);
};
// 트리 구조 관련 로직
const renderTreeCell = useCallback((row: Row<T>) => {
if (!isTreeTable || !treeConfig) return null;
return (
<div
className="relative"
style={{ paddingLeft: `${(row.depth || 0) * (treeConfig.indent || 24)}px` }}
>
{/* 트리 라인 렌더링 */}
</div>
);
}, [isTreeTable, treeConfig]);
// 행 확장 관련 로직
const renderExpandedRowContent = useCallback((row: Row<T>) => {
if (!enableExpander || !expanderConfig?.renderExpandedContent) return null;
return (
<div style={expanderConfig.expandedContentStyle}>
{expanderConfig.renderExpandedContent(row)}
</div>
);
}, [enableExpander, expanderConfig]);
// renderCell 함수를 컴포넌트로 교체
const renderCell = useCallback((cell: Cell<T, unknown>) => {
const isEditable = editableColumns?.includes(String(cell.column.id));
return (
<EditableCell
cell={cell}
isEditable={isEditable}
onCellEdit={onCellEdit}
/>
);
}, [editableColumns, onCellEdit]);
return (
<div>
<div
className="relative overflow-y-auto overflow-x-auto bg-white border border-gray-200 rounded-lg shadow-sm"
className="relative border border-gray-200 rounded-lg shadow-sm"
style={{ height: "600px" }}
>
<table className="w-full border-separate border-spacing-0 text-sm leading-normal text-gray-700 table-fixed">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="sticky top-0 z-10 bg-gray-50 text-center py-3 px-4 font-semibold text-gray-900 border-b border-gray-200 whitespace-nowrap overflow-hidden text-ellipsis"
style={{ width: header.column.columnDef.meta?.width }}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className={`transition-colors hover:bg-gray-50`}
>
{row.getVisibleCells().map((cell) => {
const isFirstCell = cell.column.id === 'expander';
return (
<td
key={cell.id}
className={`py-3 px-4 border-b border-gray-200 overflow-visible
${cell.column.columnDef.meta?.className || ""}
${isFirstCell ? 'relative' : ''}`}
style={{
width: cell.column.columnDef.meta?.width,
maxWidth: cell.column.columnDef.meta?.width,
textAlign: cell.column.columnDef.meta?.textAlign || 'left',
paddingLeft: isFirstCell ? `${row.depth * 24 + 16}px` : undefined,
}}
>
{isFirstCell && Array.from({ length: row.depth }).map((_, index) => (
<span
key={`line-${index}`}
className="absolute h-full w-px bg-gray-200"
<div className="absolute top-0 left-0 right-0 bottom-0 overflow-auto">
<table className="min-w-full border-separate border-spacing-0 text-[13px]"> {/* text-xs에서 text-[13px]로 변경 */}
<thead className="sticky top-0 z-10 bg-gray-100">
{table.getHeaderGroups().map((headerGroup, groupIndex) => {
// 헤더 그룹의 전체 개수
const totalHeaderGroups = table.getHeaderGroups().length;
return (
<tr key={headerGroup.id} className="bg-gray-50">
{headerGroup.headers.map((header) => {
const isUtilityColumn = header.column.id === 'select' || header.column.id === 'expander';
const isNestedHeader = header.column.columns?.length > 0;
// 유틸리티 컬럼(체크박스, expander)은 첫 번째 행에서만 표시하고 전체 높이로 설정
if (groupIndex > 0 && isUtilityColumn) {
return null;
}
// 부모 컬럼이 없는 일반 컬럼은 첫 번째 행에서만 표시하고 전체 높이로 설정
if (groupIndex > 0 && !header.column.parent && !isNestedHeader) {
return null;
}
// 중첩된 컬럼의 자식은 마지막 행에서만 표시
if (groupIndex < totalHeaderGroups - 1 && !isNestedHeader && header.column.parent) {
return null;
}
// rowSpan 계산
let rowSpan = 1;
if (isUtilityColumn || (!isNestedHeader && !header.column.parent)) {
rowSpan = totalHeaderGroups; // 전체 높이
} else if (!isNestedHeader && header.column.parent) {
rowSpan = 1; // 자식 컬럼은 한 줄
} else if (isNestedHeader) {
rowSpan = 1; // 그룹 헤더는 한 줄
}
return (
<th
key={header.id}
colSpan={header.colSpan}
rowSpan={rowSpan}
className={`
sticky top-0 z-10
text-center py-0.5 px-2
font-semibold text-gray-900
whitespace-nowrap overflow-hidden text-ellipsis
bg-gray-100
${isNestedHeader ? 'font-bold' : ''}
last:border-r-0
`}
style={{
left: `${(index + 1) * 24}px`,
top: 0,
zIndex: 1
width: header.column.columnDef.meta?.width,
minWidth: header.column.columnDef.meta?.width,
height: '24px', // 더 작게 조정
borderBottom: '1px solid #D1D5DB',
borderRight: '1px solid #D1D5DB',
borderTop: groupIndex === 0 ? '1px solid #D1D5DB' : 'none',
borderLeft: 'none',
}}
/>
))}
<div className={isFirstCell ? 'relative z-10' : ''}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
);
})}
</tr>
);
})}
</thead>
<tbody className="divide-y divide-gray-50">
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<tr
className="transition-colors hover:bg-gray-50"
>
{row.getVisibleCells().map((cell, cellIndex) => {
const isFirstCell = cell.column.id === 'expander';
const isSecondCell = cellIndex === 1; // expander 다음 셀 확인
const hasExpandButton = row.getCanExpand();
return (
<td
key={cell.id}
className={`py-0.5 px-3 border-b border-gray-200
${cell.column.columnDef.meta?.className || ""}
${isFirstCell ? 'relative' : ''}
${isSecondCell && hasExpandButton ? 'border-l-0' : ''}
${!isFirstCell ? 'border-r border-gray-200' : ''}`}
style={{
width: cell.column.columnDef.meta?.width,
maxWidth: cell.column.columnDef.meta?.width,
textAlign: cell.column.columnDef.meta?.textAlign || 'left',
paddingLeft: isFirstCell ? `${row.depth * 24 + 12}px` : undefined,
}}
>
{isFirstCell && row.depth > 0 && (
<>
{/* 수직 라인 */}
<span
className="absolute h-full w-px bg-gray-200"
style={{
left: '24px',
top: 0,
}}
/>
{/* 수평 라인 */}
<span
className="absolute w-4 h-px bg-gray-200"
style={{
left: '24px',
top: '50%',
}}
/>
</>
)}
<div className={`relative ${isFirstCell ? 'z-10' : ''}`}>
{renderCell(cell)}
</div>
</td>
);
})}
</tr>
{/* 확장된 행 렌더링 */}
{row.getIsExpanded() && (
<tr>
<td
colSpan={row.getVisibleCells().length}
className="border-b border-gray-200 bg-transparent"
>
<div className="relative">
{/* 왼쪽 세로 선 */}
<div
className="absolute left-0 top-0 bottom-0 w-[1px] bg-gray-200"
style={{
left: `${row.depth * 24 + 24}px`,
}}
/>
{/* 확장된 내용을 선보다 오른쪽으로 이동 */}
<div
className="ml-[48px] py-2"
style={{
width: 'calc(100% - 64px)', // 전체 너비에서 왼쪽 여백(48px)과 오른쪽 여백(16px)을 뺌
marginRight: '16px'
}}
>
{renderExpandedRowContent(row)}
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
</div>
<div className="flex items-center justify-between py-3 text-sm text-gray-700">
<div className="flex items-center justify-between py-3 text-[13px] text-gray-700"> {/* text-sm에서 text-[13px]로 변경 */}
<div className="w-1/3 flex items-center gap-2">
<span> :</span>
<select