diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index d4e8d0cf..a648a4f9 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -793,8 +793,9 @@ export const previewFile = async ( return; } - // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ) - if (companyCode !== "*" && fileRecord.company_code !== companyCode) { + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ ๊ณต๊ฐœ ์ ‘๊ทผ ์ œ์™ธ) + // ๊ณต๊ฐœ ์ ‘๊ทผ(req.user๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)์€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ—ˆ์šฉ (์ด๋ฏธ์ง€ ํ‘œ์‹œ์šฉ) + if (companyCode && companyCode !== "*" && fileRecord.company_code !== companyCode) { console.warn("โš ๏ธ ๋‹ค๋ฅธ ํšŒ์‚ฌ ํŒŒ์ผ ์ ‘๊ทผ ์‹œ๋„:", { userId: req.user?.userId, userCompanyCode: companyCode, diff --git a/backend-node/src/routes/fileRoutes.ts b/backend-node/src/routes/fileRoutes.ts index 64f02d14..4514e37f 100644 --- a/backend-node/src/routes/fileRoutes.ts +++ b/backend-node/src/routes/fileRoutes.ts @@ -24,6 +24,13 @@ const router = Router(); */ router.get("/public/:token", getFileByToken); +/** + * @route GET /api/files/preview/:objid + * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - ๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ + * @access Public + */ +router.get("/preview/:objid", previewFile); + // ๋ชจ๋“  ํŒŒ์ผ API๋Š” ์ธ์ฆ ํ•„์š” router.use(authenticateToken); @@ -64,12 +71,7 @@ router.get("/linked/:tableName/:recordId", getLinkedFiles); */ router.delete("/:objid", deleteFile); -/** - * @route GET /api/files/preview/:objid - * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - * @access Private - */ -router.get("/preview/:objid", previewFile); +// preview ๋ผ์šฐํŠธ๋Š” ์ƒ๋‹จ ๊ณต๊ฐœ ์ ‘๊ทผ ๊ตฌ์—ญ์œผ๋กœ ์ด๋™๋จ /** * @route GET /api/files/download/:objid diff --git a/docs/v2-sales-order-modal-layout.json b/docs/v2-sales-order-modal-layout.json new file mode 100644 index 00000000..6c8287e0 --- /dev/null +++ b/docs/v2-sales-order-modal-layout.json @@ -0,0 +1,557 @@ +{ + "version": "2.0", + "screenResolution": { + "width": 1400, + "height": 900, + "name": "์ˆ˜์ฃผ๋“ฑ๋ก ๋ชจ๋‹ฌ", + "category": "modal" + }, + "components": [ + { + "id": "section-options", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 20, "z": 1 }, + "size": { "width": 1360, "height": 80 }, + "overrides": { + "componentConfig": { + "title": "", + "showHeader": false, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 0 + }, + { + "id": "select-input-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "์ž…๋ ฅ ๋ฐฉ์‹", + "columnName": "input_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "customer_first", "label": "๊ฑฐ๋ž˜์ฒ˜ ์šฐ์„ " }, + { "value": "item_first", "label": "ํ’ˆ๋ชฉ ์šฐ์„ " } + ], + "placeholder": "์ž…๋ ฅ ๋ฐฉ์‹ ์„ ํƒ" + }, + "displayOrder": 1 + }, + { + "id": "select-sales-type", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 360, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "ํŒ๋งค ์œ ํ˜•", + "columnName": "sales_type", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "domestic", "label": "๊ตญ๋‚ด ํŒ๋งค" }, + { "value": "overseas", "label": "ํ•ด์™ธ ํŒ๋งค" } + ], + "placeholder": "ํŒ๋งค ์œ ํ˜• ์„ ํƒ" + }, + "displayOrder": 2 + }, + { + "id": "select-price-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 680, "y": 35, "z": 2 }, + "size": { "width": 250, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ๋ฐฉ์‹", + "columnName": "price_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "standard", "label": "๊ธฐ์ค€ ๋‹จ๊ฐ€" }, + { "value": "contract", "label": "๊ณ„์•ฝ ๋‹จ๊ฐ€" }, + { "value": "custom", "label": "๊ฐœ๋ณ„ ์ž…๋ ฅ" } + ], + "placeholder": "๋‹จ๊ฐ€ ๋ฐฉ์‹" + }, + "displayOrder": 3 + }, + { + "id": "checkbox-price-edit", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 950, "y": 35, "z": 2 }, + "size": { "width": 150, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ์ˆ˜์ • ํ—ˆ์šฉ", + "columnName": "allow_price_edit", + "mode": "check", + "source": "static", + "options": [{ "value": "Y", "label": "ํ—ˆ์šฉ" }] + }, + "displayOrder": 4 + }, + + { + "id": "section-customer-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 120 }, + "overrides": { + "componentConfig": { + "title": "๊ฑฐ๋ž˜์ฒ˜ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 5 + }, + { + "id": "select-customer", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 155, "z": 3 }, + "size": { "width": 320, "height": 40 }, + "overrides": { + "label": "๊ฑฐ๋ž˜์ฒ˜ *", + "columnName": "partner_id", + "mode": "dropdown", + "source": "entity", + "entityTable": "customer_mng", + "entityValueColumn": "customer_code", + "entityLabelColumn": "customer_name", + "searchable": true, + "placeholder": "๊ฑฐ๋ž˜์ฒ˜๋ช… ์ž…๋ ฅํ•˜์—ฌ ๊ฒ€์ƒ‰", + "required": true, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 6 + }, + { + "id": "input-manager", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 380, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‹ด๋‹น์ž", + "columnName": "manager_name", + "placeholder": "๋‹ด๋‹น์ž", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 7 + }, + { + "id": "input-delivery-partner", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 640, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์ฒ˜", + "columnName": "delivery_partner_id", + "placeholder": "๋‚ฉํ’ˆ์ฒ˜", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 8 + }, + { + "id": "input-delivery-address", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 900, "y": 155, "z": 3 }, + "size": { "width": 460, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์žฅ์†Œ", + "columnName": "delivery_address", + "placeholder": "๋‚ฉํ’ˆ์žฅ์†Œ", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 9 + }, + + { + "id": "section-item-first", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 200 }, + "overrides": { + "componentConfig": { + "title": "ํ’ˆ๋ชฉ ๋ฐ ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ์ˆ˜์ฃผ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "item_first", + "action": "show" + } + }, + "displayOrder": 10 + }, + + { + "id": "section-items", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 240, "z": 1 }, + "size": { "width": 1360, "height": 280 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€๋œ ํ’ˆ๋ชฉ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 11 + }, + { + "id": "btn-item-search", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1140, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰", + "action": { + "type": "openModal", + "modalType": "itemSelection" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 12 + }, + { + "id": "btn-shipping-plan", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1250, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "์ถœํ•˜๊ณ„ํš", + "webTypeConfig": { + "variant": "destructive" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 13 + }, + { + "id": "repeater-items", + "url": "@/lib/registry/components/v2-repeater", + "position": { "x": 40, "y": 290, "z": 3 }, + "size": { "width": 1320, "height": 200 }, + "overrides": { + "renderMode": "modal", + "dataSource": { + "tableName": "sales_order_detail", + "foreignKey": "order_no", + "referenceKey": "order_no" + }, + "columns": [ + { "field": "part_code", "header": "ํ’ˆ๋ฒˆ", "width": 100 }, + { "field": "part_name", "header": "ํ’ˆ๋ช…", "width": 150 }, + { "field": "spec", "header": "๊ทœ๊ฒฉ", "width": 100 }, + { "field": "unit", "header": "๋‹จ์œ„", "width": 80 }, + { "field": "qty", "header": "์ˆ˜๋Ÿ‰", "width": 100, "editable": true }, + { "field": "unit_price", "header": "๋‹จ๊ฐ€", "width": 100, "editable": true }, + { "field": "amount", "header": "๊ธˆ์•ก", "width": 100 }, + { "field": "due_date", "header": "๋‚ฉ๊ธฐ์ผ", "width": 120, "editable": true } + ], + "modal": { + "sourceTable": "item_info", + "sourceColumns": ["part_code", "part_name", "spec", "material", "unit_price"], + "filterCondition": {} + }, + "features": { + "showAddButton": false, + "showDeleteButton": true, + "inlineEdit": true + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 14 + }, + + { + "id": "section-trade-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 530, "z": 1 }, + "size": { "width": 1360, "height": 150 }, + "overrides": { + "componentConfig": { + "title": "๋ฌด์—ญ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 15 + }, + { + "id": "select-incoterms", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์ธ์ฝ”ํ…€์ฆˆ", + "columnName": "incoterms", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "FOB", "label": "FOB" }, + { "value": "CIF", "label": "CIF" }, + { "value": "EXW", "label": "EXW" }, + { "value": "DDP", "label": "DDP" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 16 + }, + { + "id": "select-payment-term", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 260, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๊ฒฐ์ œ ์กฐ๊ฑด", + "columnName": "payment_term", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "TT", "label": "T/T" }, + { "value": "LC", "label": "L/C" }, + { "value": "DA", "label": "D/A" }, + { "value": "DP", "label": "D/P" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 17 + }, + { + "id": "select-currency", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 480, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "ํ†ตํ™”", + "columnName": "currency", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "KRW", "label": "KRW (์›)" }, + { "value": "USD", "label": "USD (๋‹ฌ๋Ÿฌ)" }, + { "value": "EUR", "label": "EUR (์œ ๋กœ)" }, + { "value": "JPY", "label": "JPY (์—”)" }, + { "value": "CNY", "label": "CNY (์œ„์•ˆ)" } + ], + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 18 + }, + { + "id": "input-port-loading", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์„ ์ ํ•ญ", + "columnName": "port_of_loading", + "placeholder": "์„ ์ ํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 19 + }, + { + "id": "input-port-discharge", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 260, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๋„์ฐฉํ•ญ", + "columnName": "port_of_discharge", + "placeholder": "๋„์ฐฉํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 20 + }, + { + "id": "input-hs-code", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 480, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "HS Code", + "columnName": "hs_code", + "placeholder": "HS Code", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 21 + }, + + { + "id": "section-additional", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 690, "z": 1 }, + "size": { "width": 1360, "height": 130 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 22 + }, + { + "id": "input-memo", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 735, "z": 3 }, + "size": { "width": 1320, "height": 70 }, + "overrides": { + "label": "๋ฉ”๋ชจ", + "columnName": "memo", + "type": "textarea", + "placeholder": "๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 23 + }, + + { + "id": "btn-cancel", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1180, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ทจ์†Œ", + "webTypeConfig": { + "variant": "outline" + }, + "action": { + "type": "close" + } + }, + "displayOrder": 24 + }, + { + "id": "btn-save", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1280, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ €์žฅ", + "action": { + "type": "save" + } + }, + "displayOrder": 25 + } + ], + "gridSettings": { + "columns": 12, + "gap": 16, + "padding": 20, + "snapToGrid": true, + "showGrid": false + } +} diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 828d1aca..0ce2bae5 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -238,7 +238,8 @@ function ScreenViewPage() { compType?.includes("select") || compType?.includes("textarea") || compType?.includes("v2-input") || - compType?.includes("v2-select"); + compType?.includes("v2-select") || + compType?.includes("v2-media"); // ๐Ÿ†• ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 68fa0cb1..d991e553 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -604,23 +604,135 @@ export const ScreenModal: React.FC = ({ className }) => { transformOrigin: "center center", }} > - {screenData.components.map((component) => { + {(() => { + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ •์„ ์œ„ํ•ด ๋จผ์ € ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค ํŒŒ์•… + const isComponentHidden = (comp: any) => { + const cc = comp.componentConfig?.conditionalConfig || comp.conditionalConfig; + if (!cc?.enabled || !formData) return false; + + const { field, operator, value, action } = cc; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + return (action === "show" && !conditionMet) || (action === "hide" && conditionMet); + }; + + // ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ y ๋ฒ”์œ„ ์ˆ˜์ง‘ + const visibleRanges: { y: number; bottom: number }[] = []; + screenData.components.forEach((comp: any) => { + if (!isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + visibleRanges.push({ y, bottom: y + height }); + } + }); + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ์˜ "์‹ค์ œ ๋นˆ ๊ณต๊ฐ„" ๊ณ„์‚ฐ (ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜์ง€ ์•Š๋Š” ์˜์—ญ) + const getActualGap = (hiddenY: number, hiddenBottom: number): number => { + // ์ˆจ๊ฒจ์ง€๋Š” ์˜์—ญ ์ค‘ ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + let gapStart = hiddenY; + let gapEnd = hiddenBottom; + + for (const visible of visibleRanges) { + // ๊ฒน์น˜๋Š” ์˜์—ญ ํ™•์ธ + if (visible.y < gapEnd && visible.bottom > gapStart) { + // ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + if (visible.y <= gapStart && visible.bottom >= gapEnd) { + // ์™„์ „ํžˆ ๋ฎํž˜ - ๋นˆ ๊ณต๊ฐ„ ์—†์Œ + return 0; + } else if (visible.y <= gapStart) { + // ์œ„์ชฝ์ด ๋ฎํž˜ + gapStart = visible.bottom; + } else if (visible.bottom >= gapEnd) { + // ์•„๋ž˜์ชฝ์ด ๋ฎํž˜ + gapEnd = visible.y; + } + } + } + + return Math.max(0, gapEnd - gapStart); + }; + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์‹ค์ œ ๋นˆ ๊ณต๊ฐ„ ์ˆ˜์ง‘ + const hiddenGaps: { bottom: number; gap: number }[] = []; + screenData.components.forEach((comp: any) => { + if (isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + const bottom = y + height; + const gap = getActualGap(y, bottom); + if (gap > 0) { + hiddenGaps.push({ bottom, gap }); + } + } + }); + + // bottom ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ™์€ bottom์€ ๊ฐ€์žฅ ํฐ gap๋งŒ ์œ ์ง€) + const mergedGaps = new Map(); + hiddenGaps.forEach(({ bottom, gap }) => { + const existing = mergedGaps.get(bottom) || 0; + mergedGaps.set(bottom, Math.max(existing, gap)); + }); + + const sortedGaps = Array.from(mergedGaps.entries()) + .map(([bottom, gap]) => ({ bottom, gap })) + .sort((a, b) => a.bottom - b.bottom); + + console.log('๐Ÿ” [Y์กฐ์ •] visibleRanges:', visibleRanges.filter(r => r.bottom - r.y > 50).map(r => `${r.y}~${r.bottom}`)); + console.log('๐Ÿ” [Y์กฐ์ •] hiddenGaps:', sortedGaps); + + // ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ y ์กฐ์ •๊ฐ’ ๊ณ„์‚ฐ ํ•จ์ˆ˜ + const getYOffset = (compY: number, compId?: string) => { + let offset = 0; + for (const { bottom, gap } of sortedGaps) { + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆจ๊ฒจ์ง„ ์˜์—ญ ์•„๋ž˜์— ์žˆ์œผ๋ฉด ๊ทธ ๋นˆ ๊ณต๊ฐ„๋งŒํผ ์œ„๋กœ ์ด๋™ + if (compY > bottom) { + offset += gap; + } + } + if (offset > 0 && compId) { + console.log(`๐Ÿ” [Y์กฐ์ •] ${compId}: y=${compY} โ†’ ${compY - offset} (offset=${offset})`); + } + return offset; + }; + + return screenData.components.map((component: any) => { + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ์•ˆํ•จ + if (isComponentHidden(component)) { + return null; + } + // ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ offset ์กฐ์ • ๋ถˆํ•„์š” const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; + + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ • (์ˆจ๊ฒจ์ง„ ์ปดํฌ๋„ŒํŠธ ๋†’์ด๋งŒํผ ์œ„๋กœ ์ด๋™) + const compY = parseFloat(component.position?.y?.toString() || "0"); + const yAdjustment = getYOffset(compY, component.id); // offset์ด 0์ด๋ฉด ์›๋ณธ ์œ„์น˜ ์‚ฌ์šฉ (ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„ ์‚ฌ์šฉ ์‹œ) - const adjustedComponent = - offsetX === 0 && offsetY === 0 - ? component - : { - ...component, - position: { - ...component.position, - x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, - }, - }; + const adjustedComponent = { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: compY - offsetY - yAdjustment, // ๐Ÿ†• ๋™์  ์กฐ์ • ์ ์šฉ + }, + }; return ( = ({ className }) => { companyCode={user?.companyCode} /> ); - })} + }); + })()} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 735fb53c..8dc5da89 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -335,13 +335,42 @@ export const InteractiveScreenViewerDynamic: React.FC { - // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ (๊ธฐ์กด conditional ์‹œ์Šคํ…œ) const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents); // ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ˆจ๊น€ ์ฒ˜๋ฆฌ if (!conditionalResult.visible) { return null; } + + // ๐Ÿ†• conditionalConfig ์‹œ์Šคํ…œ ์ฒดํฌ (V2 ๋ ˆ์ด์•„์›ƒ์šฉ) + const conditionalConfig = (comp as any).componentConfig?.conditionalConfig; + if (conditionalConfig?.enabled && formData) { + const { field, operator, value, action } = conditionalConfig; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + if (action === "show" && !conditionMet) { + return null; + } + if (action === "hide" && conditionMet) { + return null; + } + } // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ if (isDataTableComponent(comp)) { @@ -533,11 +562,26 @@ export const InteractiveScreenViewerDynamic: React.FC = {}; + + // v2-media ์ปดํฌ๋„ŒํŠธ์˜ columnName ๋ชฉ๋ก ์ˆ˜์ง‘ + const mediaColumnNames = new Set( + allComponents + .filter((c: any) => c.componentType === "v2-media" || c.url?.includes("v2-media")) + .map((c: any) => c.columnName || c.componentConfig?.columnName) + .filter(Boolean) + ); + Object.entries(formData).forEach(([key, value]) => { - // ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋Š” ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ ์ œ์™ธ if (!Array.isArray(value)) { + // ๋ฐฐ์—ด์ด ์•„๋‹Œ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ์ €์žฅ masterFormData[key] = value; + } else if (mediaColumnNames.has(key)) { + // v2-media ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ์—ด์€ ์ฒซ ๋ฒˆ์งธ ๊ฐ’๋งŒ ์ €์žฅ (๋‹จ์ผ ํŒŒ์ผ ์ปฌ๋Ÿผ ๋Œ€์‘) + // ๋˜๋Š” JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด JSON.stringify(value) ์‚ฌ์šฉ + masterFormData[key] = value.length > 0 ? value[0] : null; + console.log(`๐Ÿ“ท ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ์ €์žฅ: ${key}, objid: ${masterFormData[key]}`); } else { console.log(`๐Ÿ”„ ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); } diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index c73e6598..192bd16c 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1623,55 +1623,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; }, [MIN_ZOOM, MAX_ZOOM]); - // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์Šค๋ƒ… + // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ (์ปดํฌ๋„ŒํŠธ ์ž๋™ ์กฐ์ • ์ œ๊ฑฐ๋จ) const updateGridSettings = useCallback( (newGridSettings: GridSettings) => { const newLayout = { ...layout, gridSettings: newGridSettings }; - - // ๊ฒฉ์ž ์Šค๋ƒ…์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ์šด ๊ฒฉ์ž์— ๋งž๊ฒŒ ์กฐ์ • - if (newGridSettings.snapToGrid && screenResolution.width > 0) { - // ์ƒˆ๋กœ์šด ๊ฒฉ์ž ์„ค์ •์œผ๋กœ ๊ฒฉ์ž ์ •๋ณด ์žฌ๊ณ„์‚ฐ (ํ•ด์ƒ๋„ ๊ธฐ์ค€) - const newGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: newGridSettings.snapToGrid || false, - }); - - const gridUtilSettings = { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: true, // ํ•ญ์ƒ 10px ์Šค๋ƒ… ํ™œ์„ฑํ™” - }; - - const adjustedComponents = layout.components.map((comp) => { - const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); - const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); - - // gridColumns๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž๋™ ์กฐ์ • - let adjustedGridColumns = comp.gridColumns; - if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > newGridSettings.columns) { - adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); - } - - return { - ...comp, - position: snappedPosition, - size: snappedSize, - gridColumns: adjustedGridColumns, // gridColumns ์†์„ฑ ์ถ”๊ฐ€/์กฐ์ • - }; - }); - - newLayout.components = adjustedComponents; - // console.log("๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๋ฐ ํฌ๊ธฐ ์ž๋™ ์กฐ์ •:", adjustedComponents.length, "๊ฐœ"); - // console.log("์ƒˆ๋กœ์šด ๊ฒฉ์ž ์ •๋ณด:", newGridInfo); - } - + // ๐Ÿ†• ๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์ž๋™ ์กฐ์ • ๋กœ์ง ์ œ๊ฑฐ๋จ + // ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ "๊ฒฉ์ž ์žฌ์กฐ์ •" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์•ผ๋งŒ ์กฐ์ •๋จ setLayout(newLayout); saveToHistory(newLayout); }, - [layout, screenResolution, saveToHistory], + [layout, saveToHistory], ); // ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์œ ์ง€) diff --git a/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx index f59171d1..e83c17f6 100644 --- a/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx @@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, X } from "lucide-react"; import { SelectTypeConfig } from "@/types/screen"; @@ -22,6 +23,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: "", allowClear: false, maxSelections: undefined, + defaultValue: "", ...config, }; @@ -32,6 +34,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); const [newOption, setNewOption] = useState({ label: "", value: "" }); @@ -53,6 +56,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); setLocalOptions( @@ -68,6 +72,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co safeConfig.placeholder, safeConfig.allowClear, safeConfig.maxSelections, + safeConfig.defaultValue, JSON.stringify(safeConfig.options), // ์˜ต์…˜ ๋ฐฐ์—ด์˜ ์ „์ฒด ๋‚ด์šฉ ๋ณ€ํ™” ๊ฐ์ง€ ]); @@ -174,6 +179,30 @@ export const SelectTypeConfigPanel: React.FC = ({ co /> + {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} +
+ + +

ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์„ ํƒ๋  ๊ฐ’

+
+ {/* ๋‹ค์ค‘ ์„ ํƒ */}