diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index fbb88750..013b2034 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -30,6 +30,7 @@ export class EntityJoinController { autoFilter, // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ž๋™ ํ•„ํ„ฐ dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ (JSON ๋ฌธ์ž์—ด) excludeFilter, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ (JSON ๋ฌธ์ž์—ด) - ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ + deduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • (JSON ๋ฌธ์ž์—ด) userLang, // userLang์€ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ search์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ํ•จ ...otherParams } = req.query; @@ -139,6 +140,24 @@ export class EntityJoinController { } } + // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ฒ˜๋ฆฌ + let parsedDeduplication: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + } | undefined = undefined; + if (deduplication) { + try { + parsedDeduplication = + typeof deduplication === "string" ? JSON.parse(deduplication) : deduplication; + logger.info("์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ํŒŒ์‹ฑ ์™„๋ฃŒ:", parsedDeduplication); + } catch (error) { + logger.warn("์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", error); + parsedDeduplication = undefined; + } + } + const result = await tableManagementService.getTableDataWithEntityJoins( tableName, { @@ -156,13 +175,26 @@ export class EntityJoinController { screenEntityConfigs: parsedScreenEntityConfigs, dataFilter: parsedDataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ excludeFilter: parsedExcludeFilter, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ „๋‹ฌ + deduplication: parsedDeduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ „๋‹ฌ } ); + // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์ฒ˜๋ฆฌ (๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ์— ์ ์šฉ) + let finalData = result; + if (parsedDeduplication?.enabled && parsedDeduplication.groupByColumn && Array.isArray(result.data)) { + logger.info(`๐Ÿ”„ ์ค‘๋ณต ์ œ๊ฑฐ ์‹œ์ž‘: ๊ธฐ์ค€ ์ปฌ๋Ÿผ = ${parsedDeduplication.groupByColumn}, ์ „๋žต = ${parsedDeduplication.keepStrategy}`); + const originalCount = result.data.length; + finalData = { + ...result, + data: this.deduplicateData(result.data, parsedDeduplication), + }; + logger.info(`โœ… ์ค‘๋ณต ์ œ๊ฑฐ ์™„๋ฃŒ: ${originalCount}๊ฐœ โ†’ ${finalData.data.length}๊ฐœ`); + } + res.status(200).json({ success: true, message: "Entity ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ฑ๊ณต", - data: result, + data: finalData, }); } catch (error) { logger.error("Entity ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", error); @@ -537,6 +569,98 @@ export class EntityJoinController { }); } } + + /** + * ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ (๋ฉ”๋ชจ๋ฆฌ ๋‚ด ์ฒ˜๋ฆฌ) + */ + private deduplicateData( + data: any[], + config: { + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + } + ): any[] { + if (!data || data.length === 0) return data; + + // ๊ทธ๋ฃน๋ณ„๋กœ ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜ + const groups: Record = {}; + + for (const row of data) { + const groupKey = row[config.groupByColumn]; + if (groupKey === undefined || groupKey === null) continue; + + if (!groups[groupKey]) { + groups[groupKey] = []; + } + groups[groupKey].push(row); + } + + // ๊ฐ ๊ทธ๋ฃน์—์„œ ํ•˜๋‚˜์˜ ํ–‰๋งŒ ์„ ํƒ + const result: any[] = []; + + for (const [groupKey, rows] of Object.entries(groups)) { + if (rows.length === 0) continue; + + let selectedRow: any; + + switch (config.keepStrategy) { + case "latest": + // ์ •๋ ฌ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ตœ์‹  (๊ฐ€์žฅ ํฐ ๊ฐ’) + if (config.sortColumn) { + rows.sort((a, b) => { + const aVal = a[config.sortColumn!]; + const bVal = b[config.sortColumn!]; + if (aVal === bVal) return 0; + if (aVal > bVal) return -1; + return 1; + }); + } + selectedRow = rows[0]; + break; + + case "earliest": + // ์ •๋ ฌ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ตœ์ดˆ (๊ฐ€์žฅ ์ž‘์€ ๊ฐ’) + if (config.sortColumn) { + rows.sort((a, b) => { + const aVal = a[config.sortColumn!]; + const bVal = b[config.sortColumn!]; + if (aVal === bVal) return 0; + if (aVal < bVal) return -1; + return 1; + }); + } + selectedRow = rows[0]; + break; + + case "base_price": + // base_price๊ฐ€ true์ธ ํ–‰ ์„ ํƒ + selectedRow = rows.find((r) => r.base_price === true || r.base_price === "true") || rows[0]; + break; + + case "current_date": + // ์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ์ค€ ์œ ํšจ ๊ธฐ๊ฐ„ ๋‚ด ํ–‰ ์„ ํƒ + const today = new Date().toISOString().split("T")[0]; + selectedRow = rows.find((r) => { + const startDate = r.start_date; + const endDate = r.end_date; + if (!startDate) return true; + if (startDate <= today && (!endDate || endDate >= today)) return true; + return false; + }) || rows[0]; + break; + + default: + selectedRow = rows[0]; + } + + if (selectedRow) { + result.push(selectedRow); + } + } + + return result; + } } export const entityJoinController = new EntityJoinController(); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 7df10fdb..6ae8f696 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -3769,6 +3769,15 @@ export class TableManagementService { const cacheableJoins: EntityJoinConfig[] = []; const dbJoins: EntityJoinConfig[] = []; + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์€ ์บ์‹œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (company_code ํ•„ํ„ฐ๋ง ํ•„์š”) + const companySpecificTables = [ + "supplier_mng", + "customer_mng", + "item_info", + "dept_info", + // ํ•„์š”์‹œ ์ถ”๊ฐ€ + ]; + for (const config of joinConfigs) { // table_column_category_values๋Š” ํŠน์ˆ˜ ์กฐ์ธ ์กฐ๊ฑด์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ํ•ญ์ƒ DB ์กฐ์ธ if (config.referenceTable === "table_column_category_values") { @@ -3777,6 +3786,13 @@ export class TableManagementService { continue; } + // ๐Ÿ”’ ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์€ ์บ์‹œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + if (companySpecificTables.includes(config.referenceTable)) { + dbJoins.push(config); + console.log(`๐Ÿ”— DB ์กฐ์ธ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ): ${config.referenceTable}`); + continue; + } + // ์บ์‹œ ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index f41c62af..f7926f43 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -174,8 +174,16 @@ export const ScreenModal: React.FC = ({ className }) => { // ๐Ÿ†• editData๊ฐ€ ์žˆ์œผ๋ฉด formData์™€ originalData๋กœ ์„ค์ • (์ˆ˜์ • ๋ชจ๋“œ) if (editData) { console.log("๐Ÿ“ [ScreenModal] ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์„ค์ •:", editData); - setFormData(editData); - setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) + + // ๐Ÿ†• ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ (๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ) vs ๋‹จ์ผ ๊ฐ์ฒด ์ฒ˜๋ฆฌ + if (Array.isArray(editData)) { + console.log(`๐Ÿ“ [ScreenModal] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ${editData.length}๊ฐœ ์„ค์ •`); + setFormData(editData as any); // ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ (SelectedItemsDetailInput์—์„œ ์ฒ˜๋ฆฌ) + setOriginalData(editData[0] || null); // ์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์›๋ณธ์œผ๋กœ ์ €์žฅ + } else { + setFormData(editData); + setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) + } } else { // ๐Ÿ†• ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ ์„ค์ • // ๐Ÿ”ง ์ค‘์š”: ์‹ ๊ทœ ๋“ฑ๋ก ์‹œ์—๋Š” ์—ฐ๊ฒฐ ํ•„๋“œ(equipment_code ๋“ฑ)๋งŒ ์ „๋‹ฌํ•ด์•ผ ํ•จ diff --git a/frontend/lib/api/entityJoin.ts b/frontend/lib/api/entityJoin.ts index a3206df9..1e84588d 100644 --- a/frontend/lib/api/entityJoin.ts +++ b/frontend/lib/api/entityJoin.ts @@ -77,6 +77,12 @@ export const entityJoinApi = { filterColumn?: string; filterValue?: any; }; // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) + deduplication?: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + }; // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • } = {}, ): Promise => { // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ์ž๋™ ํ•„ํ„ฐ๋ง ํ™œ์„ฑํ™” @@ -99,6 +105,7 @@ export const entityJoinApi = { autoFilter: JSON.stringify(autoFilter), // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง dataFilter: params.dataFilter ? JSON.stringify(params.dataFilter) : undefined, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ excludeFilter: params.excludeFilter ? JSON.stringify(params.excludeFilter) : undefined, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ + deduplication: params.deduplication ? JSON.stringify(params.deduplication) : undefined, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • }, }); return response.data.data; diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index c3991ae3..079579de 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -953,11 +953,12 @@ export const SplitPanelLayoutComponent: React.FC console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด:", searchConditions); - // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ + // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ (๐Ÿ†• deduplication ์ „๋‹ฌ) const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: searchConditions, enableEntityJoin: true, size: 1000, + deduplication: componentConfig.rightPanel?.deduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ „๋‹ฌ }); console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋ณตํ•ฉํ‚ค ์กฐํšŒ ๊ฒฐ๊ณผ:", result); @@ -1442,7 +1443,7 @@ export const SplitPanelLayoutComponent: React.FC // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ const handleEditClick = useCallback( - (panel: "left" | "right", item: any) => { + async (panel: "left" | "right", item: any) => { // ๐Ÿ†• ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ • ๋ฒ„ํŠผ ์„ค์ • ํ™•์ธ if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") { const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId; @@ -1465,11 +1466,86 @@ export const SplitPanelLayoutComponent: React.FC // ๐Ÿ†• groupByColumns ์ถ”์ถœ const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || []; - console.log("๐Ÿ”ง [SplitPanel] ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ - groupByColumns ํ™•์ธ:", { - groupByColumns, - editButtonConfig: componentConfig.rightPanel?.editButton, - hasGroupByColumns: groupByColumns.length > 0, - }); + console.log("========================================"); + console.log("๐Ÿ”ง [SplitPanel] ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ!"); + console.log("๐Ÿ”ง groupByColumns:", groupByColumns); + console.log("๐Ÿ”ง item:", item); + console.log("๐Ÿ”ง rightData:", rightData); + console.log("๐Ÿ”ง rightData length:", rightData?.length); + console.log("========================================"); + + // ๐Ÿ†• groupByColumns ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ (API ์ง์ ‘ ํ˜ธ์ถœ) + let allRelatedRecords = [item]; // ๊ธฐ๋ณธ๊ฐ’: ํ˜„์žฌ ์•„์ดํ…œ๋งŒ + + if (groupByColumns.length > 0) { + // groupByColumns ๊ฐ’์œผ๋กœ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์ƒ์„ฑ + const matchConditions: Record = {}; + groupByColumns.forEach((col: string) => { + if (item[col] !== undefined && item[col] !== null) { + matchConditions[col] = item[col]; + } + }); + + console.log("๐Ÿ” [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์‹œ์ž‘:", { + ํ…Œ์ด๋ธ”: rightTableName, + ์กฐ๊ฑด: matchConditions, + }); + + if (Object.keys(matchConditions).length > 0) { + // ๐Ÿ†• deduplication ์—†์ด ์›๋ณธ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ์กฐํšŒ (API ์ง์ ‘ ํ˜ธ์ถœ) + try { + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + + // ๐Ÿ”ง dataFilter๋กœ ์ •ํ™• ๋งค์นญ ์กฐ๊ฑด ์ƒ์„ฑ (search๋Š” LIKE ๊ฒ€์ƒ‰์ด๋ผ ๋ถ€์ •ํ™•) + const exactMatchFilters = Object.entries(matchConditions).map(([key, value]) => ({ + id: `exact-${key}`, + columnName: key, + operator: "equals", + value: value, + valueType: "text", + })); + + console.log("๐Ÿ” [SplitPanel] ์ •ํ™• ๋งค์นญ ํ•„ํ„ฐ:", exactMatchFilters); + + const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { + // search ๋Œ€์‹  dataFilter ์‚ฌ์šฉ (์ •ํ™• ๋งค์นญ) + dataFilter: { + enabled: true, + matchType: "all", + filters: exactMatchFilters, + }, + enableEntityJoin: true, + size: 1000, + // ๐Ÿ”ง ๋ช…์‹œ์ ์œผ๋กœ deduplication ๋น„ํ™œ์„ฑํ™” (๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ) + deduplication: { enabled: false, groupByColumn: "", keepStrategy: "latest" }, + }); + + // ๐Ÿ” ๋””๋ฒ„๊น…: API ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ + console.log("๐Ÿ” [SplitPanel] API ์‘๋‹ต ์ „์ฒด:", result); + console.log("๐Ÿ” [SplitPanel] result.data:", result.data); + console.log("๐Ÿ” [SplitPanel] result ํƒ€์ž…:", typeof result); + + // result ์ž์ฒด๊ฐ€ ๋ฐฐ์—ด์ผ ์ˆ˜๋„ ์žˆ์Œ (entityJoinApi ์‘๋‹ต ๊ตฌ์กฐ์— ๋”ฐ๋ผ) + const dataArray = Array.isArray(result) ? result : (result.data || []); + + if (dataArray.length > 0) { + allRelatedRecords = dataArray; + console.log("โœ… [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์™„๋ฃŒ:", { + ์กฐ๊ฑด: matchConditions, + ๊ฒฐ๊ณผ์ˆ˜: allRelatedRecords.length, + ๋ ˆ์ฝ”๋“œ๋“ค: allRelatedRecords.map((r: any) => ({ id: r.id, supplier_item_code: r.supplier_item_code })), + }); + } else { + console.warn("โš ๏ธ [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์Œ, ํ˜„์žฌ ์•„์ดํ…œ๋งŒ ์‚ฌ์šฉ"); + } + } catch (error) { + console.error("โŒ [SplitPanel] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์‹คํŒจ:", error); + allRelatedRecords = [item]; + } + } else { + console.warn("โš ๏ธ [SplitPanel] groupByColumns ๊ฐ’์ด ์—†์Œ, ํ˜„์žฌ ์•„์ดํ…œ๋งŒ ์‚ฌ์šฉ"); + } + } // ๐Ÿ”ง ์ˆ˜์ •: URL ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€์‹  editData๋กœ ์ง์ ‘ ์ „๋‹ฌ // ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ…Œ์ด๋ธ”์˜ Primary Key๊ฐ€ ๋ฌด์—‡์ด๋“  ์ƒ๊ด€์—†์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์ •ํ™•ํžˆ ์ „๋‹ฌ๋จ @@ -1477,19 +1553,21 @@ export const SplitPanelLayoutComponent: React.FC new CustomEvent("openScreenModal", { detail: { screenId: modalScreenId, - editData: item, // ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์ „๋‹ฌ - ...(groupByColumns.length > 0 && { - urlParams: { + editData: allRelatedRecords, // ๐Ÿ†• ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ ์ „๋‹ฌ (๋ฐฐ์—ด) + urlParams: { + mode: "edit", // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ ํ‘œ์‹œ + ...(groupByColumns.length > 0 && { groupByColumns: JSON.stringify(groupByColumns), - }, - }), + }), + }, }, }), ); console.log("โœ… [SplitPanel] openScreenModal ์ด๋ฒคํŠธ ๋ฐœ์ƒ (editData ์ง์ ‘ ์ „๋‹ฌ):", { screenId: modalScreenId, - editData: item, + editData: allRelatedRecords, + recordCount: allRelatedRecords.length, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "์—†์Œ", });