diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts
index 55c19353..031a1506 100644
--- a/backend-node/src/controllers/numberingRuleController.ts
+++ b/backend-node/src/controllers/numberingRuleController.ts
@@ -132,6 +132,16 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
return res.status(400).json({ success: false, error: "최소 1개 이상의 규칙 파트가 필요합니다" });
}
+ // 🆕 scopeType이 'table'인 경우 tableName 필수 체크
+ if (ruleConfig.scopeType === "table") {
+ if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") {
+ return res.status(400).json({
+ success: false,
+ error: "테이블 범위 규칙은 테이블명(tableName)이 필수입니다",
+ });
+ }
+ }
+
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", {
diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts
index e9485620..c40037bb 100644
--- a/backend-node/src/services/dynamicFormService.ts
+++ b/backend-node/src/services/dynamicFormService.ts
@@ -99,10 +99,18 @@ export class DynamicFormService {
}
try {
- // YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성
+ // YYYY-MM-DD 형식인 경우
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
- console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
- return new Date(value + "T00:00:00");
+ // DATE 타입이면 문자열 그대로 유지
+ if (lowerDataType === "date") {
+ console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`);
+ return value; // 문자열 그대로 반환
+ }
+ // TIMESTAMP 타입이면 Date 객체로 변환
+ else {
+ console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`);
+ return new Date(value + "T00:00:00");
+ }
}
// 다른 날짜 형식도 Date 객체로 변환
else {
@@ -300,13 +308,13 @@ export class DynamicFormService {
) {
// YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환
if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
- console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`);
+ console.log(`📅 날짜시간 변환: ${key} = "${value}" -> Date 객체`);
dataToInsert[key] = new Date(value);
}
- // YYYY-MM-DD 형태의 문자열을 Date 객체로 변환
+ // YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장)
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
- console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`);
- dataToInsert[key] = new Date(value + "T00:00:00");
+ console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`);
+ // dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식)
}
}
});
@@ -849,10 +857,22 @@ export class DynamicFormService {
const values: any[] = Object.values(changedFields);
values.push(id); // WHERE 조건용 ID 추가
+ // 🔑 Primary Key 타입에 맞게 캐스팅
+ const pkDataType = columnTypes[primaryKeyColumn];
+ let pkCast = '';
+ if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') {
+ pkCast = '::integer';
+ } else if (pkDataType === 'numeric' || pkDataType === 'decimal') {
+ pkCast = '::numeric';
+ } else if (pkDataType === 'uuid') {
+ pkCast = '::uuid';
+ }
+ // text, varchar 등은 캐스팅 불필요
+
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
- WHERE ${primaryKeyColumn} = $${values.length}::text
+ WHERE ${primaryKeyColumn} = $${values.length}${pkCast}
RETURNING *
`;
diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts
index 9dbe0270..a7445637 100644
--- a/backend-node/src/services/screenManagementService.ts
+++ b/backend-node/src/services/screenManagementService.ts
@@ -1418,9 +1418,9 @@ export class ScreenManagementService {
console.log(`=== 레이아웃 로드 시작 ===`);
console.log(`화면 ID: ${screenId}`);
- // 권한 확인
- const screens = await query<{ company_code: string | null }>(
- `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
+ // 권한 확인 및 테이블명 조회
+ const screens = await query<{ company_code: string | null; table_name: string | null }>(
+ `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
@@ -1512,11 +1512,13 @@ export class ScreenManagementService {
console.log(`반환할 컴포넌트 수: ${components.length}`);
console.log(`최종 격자 설정:`, gridSettings);
console.log(`최종 해상도 설정:`, screenResolution);
+ console.log(`테이블명:`, existingScreen.table_name);
return {
components,
gridSettings,
screenResolution,
+ tableName: existingScreen.table_name, // 🆕 테이블명 추가
};
}
diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts
index 38fc77b1..173de022 100644
--- a/backend-node/src/services/tableManagementService.ts
+++ b/backend-node/src/services/tableManagementService.ts
@@ -1165,6 +1165,23 @@ export class TableManagementService {
paramCount: number;
} | null> {
try {
+ // 🔧 날짜 범위 문자열 "YYYY-MM-DD|YYYY-MM-DD" 체크 (최우선!)
+ if (typeof value === "string" && value.includes("|")) {
+ const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
+ if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
+ return this.buildDateRangeCondition(columnName, value, paramIndex);
+ }
+ }
+
+ // 🔧 날짜 범위 객체 {from, to} 체크
+ if (typeof value === "object" && value !== null && ("from" in value || "to" in value)) {
+ // 날짜 범위 객체는 그대로 전달
+ const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
+ if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
+ return this.buildDateRangeCondition(columnName, value, paramIndex);
+ }
+ }
+
// 🔧 {value, operator} 형태의 필터 객체 처리
let actualValue = value;
let operator = "contains"; // 기본값
@@ -1193,6 +1210,12 @@ export class TableManagementService {
// 컬럼 타입 정보 조회
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
+ logger.info(`🔍 [buildAdvancedSearchCondition] ${tableName}.${columnName}`,
+ `webType=${columnInfo?.webType || 'NULL'}`,
+ `inputType=${columnInfo?.inputType || 'NULL'}`,
+ `actualValue=${JSON.stringify(actualValue)}`,
+ `operator=${operator}`
+ );
if (!columnInfo) {
// 컬럼 정보가 없으면 operator에 따른 기본 검색
@@ -1292,20 +1315,41 @@ export class TableManagementService {
const values: any[] = [];
let paramCount = 0;
- if (typeof value === "object" && value !== null) {
+ // 문자열 형식의 날짜 범위 파싱 ("YYYY-MM-DD|YYYY-MM-DD")
+ if (typeof value === "string" && value.includes("|")) {
+ const [fromStr, toStr] = value.split("|");
+
+ if (fromStr && fromStr.trim() !== "") {
+ // VARCHAR 컬럼을 DATE로 캐스팅하여 비교
+ conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
+ values.push(fromStr.trim());
+ paramCount++;
+ }
+ if (toStr && toStr.trim() !== "") {
+ // 종료일은 해당 날짜의 23:59:59까지 포함
+ conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
+ values.push(toStr.trim());
+ paramCount++;
+ }
+ }
+ // 객체 형식의 날짜 범위 ({from, to})
+ else if (typeof value === "object" && value !== null) {
if (value.from) {
- conditions.push(`${columnName} >= $${paramIndex + paramCount}`);
+ // VARCHAR 컬럼을 DATE로 캐스팅하여 비교
+ conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
values.push(value.from);
paramCount++;
}
if (value.to) {
- conditions.push(`${columnName} <= $${paramIndex + paramCount}`);
+ // 종료일은 해당 날짜의 23:59:59까지 포함
+ conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
values.push(value.to);
paramCount++;
}
- } else if (typeof value === "string" && value.trim() !== "") {
- // 단일 날짜 검색 (해당 날짜의 데이터)
- conditions.push(`DATE(${columnName}) = DATE($${paramIndex})`);
+ }
+ // 단일 날짜 검색
+ else if (typeof value === "string" && value.trim() !== "") {
+ conditions.push(`${columnName}::date = $${paramIndex}::date`);
values.push(value);
paramCount = 1;
}
@@ -1544,6 +1588,7 @@ export class TableManagementService {
columnName: string
): Promise<{
webType: string;
+ inputType?: string;
codeCategory?: string;
referenceTable?: string;
referenceColumn?: string;
@@ -1552,29 +1597,44 @@ export class TableManagementService {
try {
const result = await queryOne<{
web_type: string | null;
+ input_type: string | null;
code_category: string | null;
reference_table: string | null;
reference_column: string | null;
display_column: string | null;
}>(
- `SELECT web_type, code_category, reference_table, reference_column, display_column
+ `SELECT web_type, input_type, code_category, reference_table, reference_column, display_column
FROM column_labels
WHERE table_name = $1 AND column_name = $2
LIMIT 1`,
[tableName, columnName]
);
+ logger.info(`🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`, {
+ found: !!result,
+ web_type: result?.web_type,
+ input_type: result?.input_type,
+ });
+
if (!result) {
+ logger.warn(`⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`);
return null;
}
- return {
- webType: result.web_type || "",
+ // web_type이 없으면 input_type을 사용 (레거시 호환)
+ const webType = result.web_type || result.input_type || "";
+
+ const columnInfo = {
+ webType: webType,
+ inputType: result.input_type || "",
codeCategory: result.code_category || undefined,
referenceTable: result.reference_table || undefined,
referenceColumn: result.reference_column || undefined,
displayColumn: result.display_column || undefined,
};
+
+ logger.info(`✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`);
+ return columnInfo;
} catch (error) {
logger.error(
`컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,
diff --git a/backend-node/src/types/screen.ts b/backend-node/src/types/screen.ts
index 304c589c..ca5a466f 100644
--- a/backend-node/src/types/screen.ts
+++ b/backend-node/src/types/screen.ts
@@ -101,6 +101,7 @@ export interface LayoutData {
components: ComponentData[];
gridSettings?: GridSettings;
screenResolution?: ScreenResolution;
+ tableName?: string; // 🆕 화면에 연결된 테이블명
}
// 그리드 설정
diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx
index 2fb83df4..290109f3 100644
--- a/frontend/app/(main)/admin/tableMng/page.tsx
+++ b/frontend/app/(main)/admin/tableMng/page.tsx
@@ -1108,14 +1108,11 @@ export default function TableManagementPage() {
) : (
{/* 컬럼 헤더 */}
-
-
컬럼명
-
라벨
-
입력 타입
-
- 상세 설정
-
-
설명
+
{/* 컬럼 리스트 */}
@@ -1132,12 +1129,13 @@ export default function TableManagementPage() {
{columns.map((column, index) => (
-
+
-
+
handleLabelChange(column.columnName, e.target.value)}
@@ -1145,107 +1143,106 @@ export default function TableManagementPage() {
className="h-8 text-xs"
/>
-
-
-
-
- {/* 입력 타입이 'code'인 경우 공통코드 선택 */}
- {column.inputType === "code" && (
+
+
+ {/* 입력 타입 선택 */}
- )}
- {/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
- {column.inputType === "category" && (
-
-
-
- {secondLevelMenus.length === 0 ? (
-
- 2레벨 메뉴가 없습니다. 메뉴를 선택하지 않으면 모든 메뉴에서 사용 가능합니다.
-
- ) : (
- secondLevelMenus.map((menu) => {
- // menuObjid를 숫자로 변환하여 비교
- const menuObjidNum = Number(menu.menuObjid);
- const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
-
- return (
-
-
{
- const currentMenus = column.categoryMenus || [];
- const newMenus = e.target.checked
- ? [...currentMenus, menuObjidNum]
- : currentMenus.filter((id) => id !== menuObjidNum);
+ {/* 입력 타입이 'code'인 경우 공통코드 선택 */}
+ {column.inputType === "code" && (
+
+ )}
+ {/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
+ {column.inputType === "category" && (
+
+
+
+ {secondLevelMenus.length === 0 ? (
+
+ 2레벨 메뉴가 없습니다. 메뉴를 선택하지 않으면 모든 메뉴에서 사용 가능합니다.
+
+ ) : (
+ secondLevelMenus.map((menu) => {
+ // menuObjid를 숫자로 변환하여 비교
+ const menuObjidNum = Number(menu.menuObjid);
+ const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
+
+ return (
+
+ {
+ const currentMenus = column.categoryMenus || [];
+ const newMenus = e.target.checked
+ ? [...currentMenus, menuObjidNum]
+ : currentMenus.filter((id) => id !== menuObjidNum);
- setColumns((prev) =>
- prev.map((col) =>
- col.columnName === column.columnName
- ? { ...col, categoryMenus: newMenus }
- : col
- )
- );
- }}
- className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
- />
-
-
- );
- })
+ setColumns((prev) =>
+ prev.map((col) =>
+ col.columnName === column.columnName
+ ? { ...col, categoryMenus: newMenus }
+ : col
+ )
+ );
+ }}
+ className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
+ />
+
+
+ );
+ })
+ )}
+
+ {column.categoryMenus && column.categoryMenus.length > 0 && (
+
+ {column.categoryMenus.length}개 메뉴 선택됨
+
)}
- {column.categoryMenus && column.categoryMenus.length > 0 && (
-
- {column.categoryMenus.length}개 메뉴 선택됨
-
- )}
-
- )}
- {/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
- {column.inputType === "entity" && (
-
-
+ )}
+ {/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
+ {column.inputType === "entity" && (
+ <>
{/* 참조 테이블 */}
-
+
@@ -1255,7 +1252,7 @@ export default function TableManagementPage() {
handleDetailSettingsChange(column.columnName, "entity", value)
}
>
-
+
@@ -1278,7 +1275,7 @@ export default function TableManagementPage() {
{/* 조인 컬럼 */}
{column.referenceTable && column.referenceTable !== "none" && (
-
+
@@ -1292,7 +1289,7 @@ export default function TableManagementPage() {
)
}
>
-
+
@@ -1324,7 +1321,7 @@ export default function TableManagementPage() {
column.referenceTable !== "none" &&
column.referenceColumn &&
column.referenceColumn !== "none" && (
-
+
@@ -1338,7 +1335,7 @@ export default function TableManagementPage() {
)
}
>
-
+
@@ -1364,37 +1361,29 @@ export default function TableManagementPage() {
)}
-
- {/* 설정 완료 표시 */}
- {column.referenceTable &&
- column.referenceTable !== "none" &&
- column.referenceColumn &&
- column.referenceColumn !== "none" &&
- column.displayColumn &&
- column.displayColumn !== "none" && (
-
- ✓
-
- {column.columnName} → {column.referenceTable}.{column.displayColumn}
-
-
- )}
-
- )}
- {/* 다른 입력 타입인 경우 빈 공간 */}
- {column.inputType !== "code" && column.inputType !== "category" && column.inputType !== "entity" && (
-
- -
-
- )}
+ {/* 설정 완료 표시 */}
+ {column.referenceTable &&
+ column.referenceTable !== "none" &&
+ column.referenceColumn &&
+ column.referenceColumn !== "none" &&
+ column.displayColumn &&
+ column.displayColumn !== "none" && (
+
+ ✓
+ 설정 완료
+
+ )}
+ >
+ )}
+
-
@@ -1585,3 +1574,4 @@ export default function TableManagementPage() {
);
}
+
diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx
index ce99a685..5685d23a 100644
--- a/frontend/app/(main)/screens/[screenId]/page.tsx
+++ b/frontend/app/(main)/screens/[screenId]/page.tsx
@@ -356,17 +356,6 @@ function ScreenViewPage() {
return isButton;
});
- console.log(
- "🔍 메뉴에서 발견된 전체 버튼:",
- allButtons.map((b) => ({
- id: b.id,
- label: b.label,
- positionX: b.position.x,
- positionY: b.position.y,
- width: b.size?.width,
- height: b.size?.height,
- })),
- );
topLevelComponents.forEach((component) => {
const isButton =
@@ -406,33 +395,13 @@ function ScreenViewPage() {
(c) => (c as any).componentId === "table-search-widget",
);
- // 디버그: 모든 컴포넌트 타입 확인
- console.log(
- "🔍 전체 컴포넌트 타입:",
- regularComponents.map((c) => ({
- id: c.id,
- type: c.type,
- componentType: (c as any).componentType,
- componentId: (c as any).componentId,
- })),
- );
-
- // 🆕 조건부 컨테이너들을 찾기
+ // 조건부 컨테이너들을 찾기
const conditionalContainers = regularComponents.filter(
(c) =>
(c as any).componentId === "conditional-container" ||
(c as any).componentType === "conditional-container",
);
- console.log(
- "🔍 조건부 컨테이너 발견:",
- conditionalContainers.map((c) => ({
- id: c.id,
- y: c.position.y,
- size: c.size,
- })),
- );
-
// TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정
const adjustedComponents = regularComponents.map((component) => {
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
@@ -520,12 +489,6 @@ function ScreenViewPage() {
columnOrder={tableColumnOrder}
tableDisplayData={tableDisplayData}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => {
- console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
- console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder });
- console.log("📊 화면 표시 데이터:", {
- count: tableDisplayData?.length,
- firstRow: tableDisplayData?.[0],
- });
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
@@ -604,12 +567,6 @@ function ScreenViewPage() {
columnOrder,
tableDisplayData,
) => {
- console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
- console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder });
- console.log("📊 화면 표시 데이터 (자식):", {
- count: tableDisplayData?.length,
- firstRow: tableDisplayData?.[0],
- });
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
@@ -618,7 +575,6 @@ function ScreenViewPage() {
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
- console.log("🔄 테이블 새로고침 요청됨 (자식)");
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
index 0bd49982..bfdb69c2 100644
--- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
+++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
@@ -152,7 +152,7 @@ export const NumberingRuleDesigner: React.FC
= ({
const ruleToSave = {
...currentRule,
scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지
- tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정
+ tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 자동 설정 (빈 값은 null)
menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
};
diff --git a/frontend/components/order/OrderItemRepeaterTable.tsx b/frontend/components/order/OrderItemRepeaterTable.tsx
index dd38ee5a..dbfe5eee 100644
--- a/frontend/components/order/OrderItemRepeaterTable.tsx
+++ b/frontend/components/order/OrderItemRepeaterTable.tsx
@@ -75,6 +75,13 @@ const ORDER_COLUMNS: RepeaterColumnConfig[] = [
calculated: true,
width: "120px",
},
+ {
+ field: "order_date",
+ label: "수주일",
+ type: "date",
+ editable: true,
+ width: "130px",
+ },
{
field: "delivery_date",
label: "납기일",
diff --git a/frontend/components/order/OrderRegistrationModal.tsx b/frontend/components/order/OrderRegistrationModal.tsx
index bd780038..615f0426 100644
--- a/frontend/components/order/OrderRegistrationModal.tsx
+++ b/frontend/components/order/OrderRegistrationModal.tsx
@@ -64,6 +64,9 @@ export function OrderRegistrationModal({
// 선택된 품목 목록
const [selectedItems, setSelectedItems] = useState([]);
+ // 납기일 일괄 적용 플래그 (딱 한 번만 실행)
+ const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false);
+
// 저장 중
const [isSaving, setIsSaving] = useState(false);
@@ -158,6 +161,45 @@ export function OrderRegistrationModal({
hsCode: "",
});
setSelectedItems([]);
+ setIsDeliveryDateApplied(false); // 플래그 초기화
+ };
+
+ // 품목 목록 변경 핸들러 (납기일 일괄 적용 로직 포함)
+ const handleItemsChange = (newItems: any[]) => {
+ // 1️⃣ 플래그가 이미 true면 그냥 업데이트만 (일괄 적용 완료 상태)
+ if (isDeliveryDateApplied) {
+ setSelectedItems(newItems);
+ return;
+ }
+
+ // 2️⃣ 품목이 없으면 그냥 업데이트
+ if (newItems.length === 0) {
+ setSelectedItems(newItems);
+ return;
+ }
+
+ // 3️⃣ 현재 상태: 납기일이 있는 행과 없는 행 개수 체크
+ const itemsWithDate = newItems.filter((item) => item.delivery_date);
+ const itemsWithoutDate = newItems.filter((item) => !item.delivery_date);
+
+ // 4️⃣ 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용
+ if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) {
+ // 5️⃣ 전체 일괄 적용
+ const selectedDate = itemsWithDate[0].delivery_date;
+ const updatedItems = newItems.map((item) => ({
+ ...item,
+ delivery_date: selectedDate, // 모든 행에 동일한 납기일 적용
+ }));
+
+ setSelectedItems(updatedItems);
+ setIsDeliveryDateApplied(true); // 플래그 활성화 (다음부터는 일괄 적용 안 함)
+
+ console.log("✅ 납기일 일괄 적용 완료:", selectedDate);
+ console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`);
+ } else {
+ // 그냥 업데이트
+ setSelectedItems(newItems);
+ }
};
// 전체 금액 계산
@@ -338,7 +380,7 @@ export function OrderRegistrationModal({
diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx
index f9b803b2..9945a19c 100644
--- a/frontend/components/screen/EditModal.tsx
+++ b/frontend/components/screen/EditModal.tsx
@@ -316,6 +316,33 @@ export const EditModal: React.FC
= ({ className }) => {
screenId: modalState.screenId,
});
+ // 🆕 날짜 필드 정규화 함수 (YYYY-MM-DD 형식으로 변환)
+ const normalizeDateField = (value: any): string | null => {
+ if (!value) return null;
+
+ // ISO 8601 형식 (2025-11-26T00:00:00.000Z) 또는 Date 객체
+ if (value instanceof Date || typeof value === "string") {
+ try {
+ const date = new Date(value);
+ if (isNaN(date.getTime())) return null;
+
+ // YYYY-MM-DD 형식으로 변환
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ } catch (error) {
+ console.warn("날짜 변환 실패:", value, error);
+ return null;
+ }
+ }
+
+ return null;
+ };
+
+ // 날짜 필드 목록
+ const dateFields = ["item_due_date", "delivery_date", "due_date", "order_date"];
+
let insertedCount = 0;
let updatedCount = 0;
let deletedCount = 0;
@@ -333,6 +360,17 @@ export const EditModal: React.FC = ({ className }) => {
delete insertData.id; // id는 자동 생성되므로 제거
+ // 🆕 날짜 필드 정규화 (YYYY-MM-DD 형식으로 변환)
+ dateFields.forEach((fieldName) => {
+ if (insertData[fieldName]) {
+ const normalizedDate = normalizeDateField(insertData[fieldName]);
+ if (normalizedDate) {
+ insertData[fieldName] = normalizedDate;
+ console.log(`📅 [날짜 정규화] ${fieldName}: ${currentData[fieldName]} → ${normalizedDate}`);
+ }
+ }
+ });
+
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
modalState.groupByColumns.forEach((colName) => {
@@ -348,23 +386,32 @@ export const EditModal: React.FC = ({ className }) => {
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
// formData에서 품목별 필드가 아닌 공통 필드를 복사
const commonFields = [
- 'partner_id', // 거래처
- 'manager_id', // 담당자
- 'delivery_partner_id', // 납품처
- 'delivery_address', // 납품장소
- 'memo', // 메모
- 'order_date', // 주문일
- 'due_date', // 납기일
- 'shipping_method', // 배송방법
- 'status', // 상태
- 'sales_type', // 영업유형
+ "partner_id", // 거래처
+ "manager_id", // 담당자
+ "delivery_partner_id", // 납품처
+ "delivery_address", // 납품장소
+ "memo", // 메모
+ "order_date", // 주문일
+ "due_date", // 납기일
+ "shipping_method", // 배송방법
+ "status", // 상태
+ "sales_type", // 영업유형
];
commonFields.forEach((fieldName) => {
// formData에 값이 있으면 추가
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
- insertData[fieldName] = formData[fieldName];
- console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
+ // 날짜 필드인 경우 정규화
+ if (dateFields.includes(fieldName)) {
+ const normalizedDate = normalizeDateField(formData[fieldName]);
+ if (normalizedDate) {
+ insertData[fieldName] = normalizedDate;
+ console.log(`🔗 [공통 필드 - 날짜] ${fieldName} 값 추가:`, normalizedDate);
+ }
+ } else {
+ insertData[fieldName] = formData[fieldName];
+ console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
+ }
}
});
@@ -404,8 +451,15 @@ export const EditModal: React.FC = ({ className }) => {
}
// 🆕 값 정규화 함수 (타입 통일)
- const normalizeValue = (val: any): any => {
+ const normalizeValue = (val: any, fieldName?: string): any => {
if (val === null || val === undefined || val === "") return null;
+
+ // 날짜 필드인 경우 YYYY-MM-DD 형식으로 정규화
+ if (fieldName && dateFields.includes(fieldName)) {
+ const normalizedDate = normalizeDateField(val);
+ return normalizedDate;
+ }
+
if (typeof val === "string" && !isNaN(Number(val))) {
// 숫자로 변환 가능한 문자열은 숫자로
return Number(val);
@@ -422,13 +476,14 @@ export const EditModal: React.FC = ({ className }) => {
}
// 🆕 타입 정규화 후 비교
- const currentValue = normalizeValue(currentData[key]);
- const originalValue = normalizeValue(originalItemData[key]);
+ const currentValue = normalizeValue(currentData[key], key);
+ const originalValue = normalizeValue(originalItemData[key], key);
// 값이 변경된 경우만 포함
if (currentValue !== originalValue) {
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`);
- changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로)
+ // 날짜 필드는 정규화된 값 사용, 나머지는 원본 값 사용
+ changedData[key] = dateFields.includes(key) ? currentValue : currentData[key];
}
});
@@ -631,13 +686,6 @@ export const EditModal: React.FC = ({ className }) => {
maxHeight: "100%",
}}
>
- {/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */}
- {groupData.length > 1 && (
-
- {groupData.length}개의 관련 품목을 함께 수정합니다
-
- )}
-
{screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정
const offsetX = screenDimensions?.offsetX || 0;
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx
index 4c3e6506..8e1f1ce3 100644
--- a/frontend/components/screen/InteractiveScreenViewer.tsx
+++ b/frontend/components/screen/InteractiveScreenViewer.tsx
@@ -433,7 +433,10 @@ export const InteractiveScreenViewer: React.FC = (
return (
-
+
);
}
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
index aa46ed40..41e321e5 100644
--- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
+++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
@@ -39,6 +39,7 @@ interface InteractiveScreenViewerProps {
id: number;
tableName?: string;
};
+ menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
onSave?: () => Promise;
onRefresh?: () => void;
onFlowRefresh?: () => void;
@@ -61,6 +62,7 @@ export const InteractiveScreenViewerDynamic: React.FC
화면 미리보기 - {screenToPreview?.screenName}
-
- {isLoadingPreview ? (
-
-
-
레이아웃 로딩 중...
-
화면 정보를 불러오고 있습니다.
+
+
+
+ {isLoadingPreview ? (
+
+
+
레이아웃 로딩 중...
+
화면 정보를 불러오고 있습니다.
+
-
- ) : previewLayout && previewLayout.components ? (
+ ) : previewLayout && previewLayout.components ? (
(() => {
const screenWidth = previewLayout.screenResolution?.width || 1200;
const screenHeight = previewLayout.screenResolution?.height || 800;
// 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외)
- const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩
+ const modalPadding = 100; // 헤더 + 푸터 + 패딩
+ const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - modalPadding : 1700;
+ const availableHeight = typeof window !== "undefined" ? window.innerHeight * 0.95 - modalPadding : 900;
- // 가로폭 기준으로 스케일 계산 (가로폭에 맞춤)
- const scale = availableWidth / screenWidth;
+ // 가로/세로 비율을 모두 고려하여 작은 쪽에 맞춤 (화면이 잘리지 않도록)
+ const scaleX = availableWidth / screenWidth;
+ const scaleY = availableHeight / screenHeight;
+ const scale = Math.min(scaleX, scaleY, 1); // 최대 1배율 (확대 방지)
+
+ console.log("📐 미리보기 스케일 계산:", {
+ screenWidth,
+ screenHeight,
+ availableWidth,
+ availableHeight,
+ scaleX,
+ scaleY,
+ finalScale: scale,
+ });
return (
- {/* 라벨을 외부에 별도로 렌더링 */}
- {shouldShowLabel && (
-
- {labelText}
- {component.required && *}
-
- )}
+
{}}
+ screenId={screenToPreview!.screenId}
+ tableName={screenToPreview?.tableName}
+ formData={previewFormData}
+ onFormDataChange={(fieldName, value) => {
+ setPreviewFormData((prev) => ({
+ ...prev,
+ [fieldName]: value,
+ }));
+ }}
+ >
+ {/* 자식 컴포넌트들 */}
+ {(component.type === "group" ||
+ component.type === "container" ||
+ component.type === "area") &&
+ previewLayout.components
+ .filter((child: any) => child.parentId === component.id)
+ .map((child: any) => {
+ // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
+ const relativeChildComponent = {
+ ...child,
+ position: {
+ x: child.position.x - component.position.x,
+ y: child.position.y - component.position.y,
+ z: child.position.z || 1,
+ },
+ };
- {/* 실제 컴포넌트 */}
- {
- const style = {
- position: "absolute" as const,
- left: `${component.position.x}px`,
- top: `${component.position.y}px`,
- width: component.style?.width || `${component.size.width}px`,
- height: component.style?.height || `${component.size.height}px`,
- zIndex: component.position.z || 1,
- };
-
- return style;
- })()}
- >
- {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
- {component.type !== "widget" ? (
- {
- setPreviewFormData((prev) => ({
- ...prev,
- [fieldName]: value,
- }));
- }}
- screenId={screenToPreview!.screenId}
- tableName={screenToPreview?.tableName}
- />
- ) : (
- {
- // 유틸리티 함수로 파일 컴포넌트 감지
- if (isFileComponent(component)) {
- return "file";
- }
- // 다른 컴포넌트는 유틸리티 함수로 webType 결정
- return getComponentWebType(component) || "text";
- })()}
- config={component.webTypeConfig}
- props={{
- component: component,
- value: previewFormData[component.columnName || component.id] || "",
- onChange: (value: any) => {
- const fieldName = component.columnName || component.id;
- setPreviewFormData((prev) => ({
- ...prev,
- [fieldName]: value,
- }));
- },
- onFormDataChange: (fieldName, value) => {
- setPreviewFormData((prev) => ({
- ...prev,
- [fieldName]: value,
- }));
- },
- isInteractive: true,
- formData: previewFormData,
- readonly: component.readonly,
- required: component.required,
- placeholder: component.placeholder,
- className: "w-full h-full",
- }}
- />
- )}
-
-
+ return (
+ {}}
+ screenId={screenToPreview!.screenId}
+ tableName={screenToPreview?.tableName}
+ formData={previewFormData}
+ onFormDataChange={(fieldName, value) => {
+ setPreviewFormData((prev) => ({
+ ...prev,
+ [fieldName]: value,
+ }));
+ }}
+ />
+ );
+ })}
+
);
})}
@@ -1536,7 +1501,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
)}
-
+
+
+