diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts
index e275d825..51ab567a 100644
--- a/backend-node/src/services/dataService.ts
+++ b/backend-node/src/services/dataService.ts
@@ -18,45 +18,6 @@ import { pool } from "../database/db"; // ๐ Entity ์กฐ์ธ์ ์ํ pool impo
import { buildDataFilterWhereClause } from "../utils/dataFilterUtil"; // ๐ ๋ฐ์ดํฐ ํํฐ ์ ํธ
import { v4 as uuidv4 } from "uuid"; // ๐ UUID ์์ฑ
-/**
- * ๋น๋ฐ๋ฒํธ(password) ํ์
์ปฌ๋ผ์ ๊ฐ์ ๋น ๋ฌธ์์ด๋ก ๋ง์คํน
- * - table_type_columns์์ input_type = 'password'์ธ ์ปฌ๋ผ์ ์กฐํ
- * - ๋ฐ์ดํฐ ์๋ต์์ ํด๋น ์ปฌ๋ผ ๊ฐ์ ๋น์์ ํด์๊ฐ ๋
ธ์ถ ๋ฐฉ์ง
- */
-async function maskPasswordColumns(tableName: string, data: any): Promise {
- try {
- const passwordCols = await query<{ column_name: string }>(
- `SELECT DISTINCT column_name FROM table_type_columns
- WHERE table_name = $1 AND input_type = 'password'`,
- [tableName]
- );
- if (passwordCols.length === 0) return data;
-
- const passwordColumnNames = new Set(passwordCols.map(c => c.column_name));
-
- // ๋จ์ผ ๊ฐ์ฒด ์ฒ๋ฆฌ
- const maskRow = (row: any) => {
- if (!row || typeof row !== "object") return row;
- const masked = { ...row };
- for (const col of passwordColumnNames) {
- if (col in masked) {
- masked[col] = ""; // ํด์๊ฐ ๋์ ๋น ๋ฌธ์์ด
- }
- }
- return masked;
- };
-
- if (Array.isArray(data)) {
- return data.map(maskRow);
- }
- return maskRow(data);
- } catch (error) {
- // ๋ง์คํน ์คํจํด๋ ์๋ณธ ๋ฐ์ดํฐ ๋ฐํ (์๋น์ค ์ค๋จ ๋ฐฉ์ง)
- console.warn("โ ๏ธ password ์ปฌ๋ผ ๋ง์คํน ์คํจ:", error);
- return data;
- }
-}
-
interface GetTableDataParams {
tableName: string;
limit?: number;
@@ -661,14 +622,14 @@ class DataService {
return {
success: true,
- data: await maskPasswordColumns(tableName, normalizedGroupRows), // ๐ง ๋ฐฐ์ด๋ก ๋ฐํ! + password ๋ง์คํน
+ data: normalizedGroupRows, // ๐ง ๋ฐฐ์ด๋ก ๋ฐํ!
};
}
}
return {
success: true,
- data: await maskPasswordColumns(tableName, normalizedRows[0]), // ๊ทธ๋ฃนํ ์์ผ๋ฉด ๋จ์ผ ๋ ์ฝ๋ + password ๋ง์คํน
+ data: normalizedRows[0], // ๊ทธ๋ฃนํ ์์ผ๋ฉด ๋จ์ผ ๋ ์ฝ๋
};
}
}
@@ -687,7 +648,7 @@ class DataService {
return {
success: true,
- data: await maskPasswordColumns(tableName, result[0]), // password ๋ง์คํน
+ data: result[0],
};
} catch (error) {
console.error(`๋ ์ฝ๋ ์์ธ ์กฐํ ์ค๋ฅ (${tableName}/${id}):`, error);
diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts
index ac2377fe..e1242afd 100644
--- a/backend-node/src/services/dynamicFormService.ts
+++ b/backend-node/src/services/dynamicFormService.ts
@@ -2,7 +2,6 @@ import { query, queryOne, transaction, getPool } from "../database/db";
import { EventTriggerService } from "./eventTriggerService";
import { DataflowControlService } from "./dataflowControlService";
import tableCategoryValueService from "./tableCategoryValueService";
-import { PasswordUtils } from "../utils/passwordUtils";
export interface FormDataResult {
id: number;
@@ -860,33 +859,6 @@ export class DynamicFormService {
}
}
- // ๋น๋ฐ๋ฒํธ(password) ํ์
์ปฌ๋ผ ์ฒ๋ฆฌ
- // - ๋น ๊ฐ์ด๋ฉด ๋ณ๊ฒฝ ๋ชฉ๋ก์์ ์ ๊ฑฐ (๊ธฐ์กด ๋น๋ฐ๋ฒํธ ์ ์ง)
- // - ๊ฐ์ด ์์ผ๋ฉด ์ํธํ ํ ์ ์ฅ
- try {
- const passwordCols = await query<{ column_name: string }>(
- `SELECT DISTINCT column_name FROM table_type_columns
- WHERE table_name = $1 AND input_type = 'password'`,
- [tableName]
- );
- for (const { column_name } of passwordCols) {
- if (column_name in changedFields) {
- const pwValue = changedFields[column_name];
- if (!pwValue || pwValue === "") {
- // ๋น ๊ฐ โ ๊ธฐ์กด ๋น๋ฐ๋ฒํธ ์ ์ง (๋ณ๊ฒฝ ๋ชฉ๋ก์์ ์ ๊ฑฐ)
- delete changedFields[column_name];
- console.log(`๐ ๋น๋ฐ๋ฒํธ ํ๋ ${column_name}: ๋น ๊ฐ์ด๋ฏ๋ก ์
๋ฐ์ดํธ ์คํต (๊ธฐ์กด ์ ์ง)`);
- } else {
- // ๊ฐ ์์ โ ์ํธํํ์ฌ ์ ์ฅ
- changedFields[column_name] = PasswordUtils.encrypt(pwValue);
- console.log(`๐ ๋น๋ฐ๋ฒํธ ํ๋ ${column_name}: ์ ๋น๋ฐ๋ฒํธ ์ํธํ ์๋ฃ`);
- }
- }
- }
- } catch (pwError) {
- console.warn("โ ๏ธ ๋น๋ฐ๋ฒํธ ์ปฌ๋ผ ์ฒ๋ฆฌ ์ค ์ค๋ฅ:", pwError);
- }
-
// ๋ณ๊ฒฝ๋ ํ๋๊ฐ ์์ผ๋ฉด ์
๋ฐ์ดํธ ๊ฑด๋๋ฐ๊ธฐ
if (Object.keys(changedFields).length === 0) {
console.log("๐ ๋ณ๊ฒฝ๋ ํ๋๊ฐ ์์ต๋๋ค. ์
๋ฐ์ดํธ๋ฅผ ๊ฑด๋๋๋๋ค.");
diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts
index 87e2ece6..2c25f7e0 100644
--- a/backend-node/src/services/screenManagementService.ts
+++ b/backend-node/src/services/screenManagementService.ts
@@ -5177,8 +5177,18 @@ export class ScreenManagementService {
throw new Error("์ด ํ๋ฉด์ ๋ ์ด์์์ ์ ์ฅํ ๊ถํ์ด ์์ต๋๋ค.");
}
+ // ํ๋ฉด์ ๊ธฐ๋ณธ ํ
์ด๋ธ ์
๋ฐ์ดํธ (ํ
์ด๋ธ์ด ์ ํ๋ ๊ฒฝ์ฐ)
+ const mainTableName = layoutData.mainTableName;
+ if (mainTableName) {
+ await query(
+ `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
+ [mainTableName, screenId],
+ );
+ console.log(`โ
[saveLayoutV2] ํ๋ฉด ๊ธฐ๋ณธ ํ
์ด๋ธ ์
๋ฐ์ดํธ: ${mainTableName}`);
+ }
+
// ์ ์ฅํ layout_data์์ ๋ ์ด์ด ๋ฉํ ์ ๋ณด ์ ๊ฑฐ (์์ ๋ ์ด์์๋ง ์ ์ฅ)
- const { layerId: _lid, layerName: _ln, conditionConfig: _cc, ...pureLayoutData } = layoutData;
+ const { layerId: _lid, layerName: _ln, conditionConfig: _cc, mainTableName: _mtn, ...pureLayoutData } = layoutData;
const dataToSave = {
version: "2.0",
...pureLayoutData,
diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx
index 138f560c..b6660709 100644
--- a/frontend/components/common/ScreenModal.tsx
+++ b/frontend/components/common/ScreenModal.tsx
@@ -554,6 +554,16 @@ export const ScreenModal: React.FC = ({ className }) => {
// ํ๋ฉด ๊ด๋ฆฌ์์ ์ค์ ํ ํด์๋ ์ฌ์ฉ (์ฐ์ ์์)
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
+ console.log("๐ [ScreenModal] ํด์๋ ๋๋ฒ๊ทธ:", {
+ screenId,
+ v2ScreenResolution: v2LayoutData?.screenResolution,
+ layoutScreenResolution: (layoutData as any).screenResolution,
+ screenInfoResolution: (screenInfo as any).screenResolution,
+ finalScreenResolution: screenResolution,
+ hasWidth: screenResolution?.width,
+ hasHeight: screenResolution?.height,
+ });
+
let dimensions;
if (screenResolution && screenResolution.width && screenResolution.height) {
// ํ๋ฉด ๊ด๋ฆฌ์์ ์ค์ ํ ํด์๋ ์ฌ์ฉ
@@ -563,9 +573,11 @@ export const ScreenModal: React.FC = ({ className }) => {
offsetX: 0,
offsetY: 0,
};
+ console.log("โ
[ScreenModal] ํ๋ฉด๊ด๋ฆฌ ํด์๋ ์ ์ฉ:", dimensions);
} else {
// ํด์๋ ์ ๋ณด๊ฐ ์์ผ๋ฉด ์๋ ๊ณ์ฐ
dimensions = calculateScreenDimensions(components);
+ console.log("โ ๏ธ [ScreenModal] ํด์๋ ์์ - ์๋ ๊ณ์ฐ:", dimensions);
}
setScreenDimensions(dimensions);
@@ -869,16 +881,24 @@ export const ScreenModal: React.FC = ({ className }) => {
// ๋ชจ๋ฌ ํฌ๊ธฐ ์ค์ - ํ๋ฉด๊ด๋ฆฌ ์ค์ ํฌ๊ธฐ + ํค๋/ํธํฐ
const getModalStyle = () => {
if (!screenDimensions) {
+ console.log("โ ๏ธ [ScreenModal] getModalStyle: screenDimensions๊ฐ null - ๊ธฐ๋ณธ ์คํ์ผ ์ฌ์ฉ");
return {
className: "w-fit min-w-[400px] max-w-4xl overflow-hidden",
style: { padding: 0, gap: 0, maxHeight: "calc(100dvh - 8px)" },
};
}
+ const finalWidth = Math.min(screenDimensions.width, window.innerWidth * 0.98);
+ console.log("โ
[ScreenModal] getModalStyle: ํด์๋ ์ ์ฉ๋จ", {
+ screenDimensions,
+ finalWidth: `${finalWidth}px`,
+ viewportWidth: window.innerWidth,
+ });
+
return {
className: "overflow-hidden",
style: {
- width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`,
+ width: `${finalWidth}px`,
// CSS๊ฐ ์์์ ์ฒ๋ฆฌ: ๋ทฐํฌํธ ์์ ๋ค์ด๊ฐ๋ฉด auto-height, ๋์น๋ฉด max-height๋ก ์ ํ
maxHeight: "calc(100dvh - 8px)",
maxWidth: "98vw",
diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx
index d8ce8e7a..0fd0cfec 100644
--- a/frontend/components/screen/EditModal.tsx
+++ b/frontend/components/screen/EditModal.tsx
@@ -565,12 +565,32 @@ export const EditModal: React.FC = ({ className }) => {
return newActiveIds;
}, [formData, groupData, conditionalLayers, screenData?.components]);
- // ๐ ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ๊ฐ์ ธ์ค๊ธฐ
+ // ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ๊ฐ์ ธ์ค๊ธฐ (Zone ์คํ์
์ ์ฉ)
const activeConditionalComponents = useMemo(() => {
return conditionalLayers
.filter((layer) => activeConditionalLayerIds.includes(layer.id))
- .flatMap((layer) => (layer as LayerDefinition & { components: ComponentData[] }).components || []);
- }, [conditionalLayers, activeConditionalLayerIds]);
+ .flatMap((layer) => {
+ const layerWithComps = layer as LayerDefinition & { components: ComponentData[] };
+ const comps = layerWithComps.components || [];
+
+ // Zone ์คํ์
์ ์ฉ: ์กฐ๊ฑด๋ถ ๋ ์ด์ด ์ปดํฌ๋ํธ๋ Zone ๋ด๋ถ ์๋ ์ขํ๋ก ์ ์ฅ๋๋ฏ๋ก
+ // Zone์ ์ ๋ ์ขํ๋ฅผ ๋ํด์ค์ผ EditModal์์ ์ฌ๋ฐ๋ฅธ ์์น์ ๋ ๋๋ง๋จ
+ const associatedZone = zones.find((z) => z.zone_id === (layer as any).zoneId);
+ if (!associatedZone) return comps;
+
+ const zoneOffsetX = associatedZone.x || 0;
+ const zoneOffsetY = associatedZone.y || 0;
+
+ return comps.map((comp) => ({
+ ...comp,
+ position: {
+ ...comp.position,
+ x: parseFloat(comp.position?.x?.toString() || "0") + zoneOffsetX,
+ y: parseFloat(comp.position?.y?.toString() || "0") + zoneOffsetY,
+ },
+ }));
+ });
+ }, [conditionalLayers, activeConditionalLayerIds, zones]);
const handleClose = () => {
setModalState({
@@ -881,14 +901,31 @@ export const EditModal: React.FC = ({ className }) => {
}
}
+ // V2Repeater ์ ์ฅ ์ด๋ฒคํธ ๋ฐ์ (๋ํ
์ผ ํ
์ด๋ธ ๋ฐ์ดํฐ ์ ์ฅ)
+ const hasRepeaterInstances = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
+ if (hasRepeaterInstances) {
+ const masterRecordId = groupData[0]?.id || formData.id;
+ window.dispatchEvent(
+ new CustomEvent("repeaterSave", {
+ detail: {
+ parentId: masterRecordId,
+ masterRecordId,
+ mainFormData: formData,
+ tableName: screenData.screenInfo.tableName,
+ },
+ }),
+ );
+ console.log("๐ [EditModal] ๊ทธ๋ฃน ์ ์ฅ ํ repeaterSave ์ด๋ฒคํธ ๋ฐ์:", { masterRecordId });
+ }
+
// ๊ฒฐ๊ณผ ๋ฉ์์ง
const messages: string[] = [];
if (insertedCount > 0) messages.push(`${insertedCount}๊ฐ ์ถ๊ฐ`);
if (updatedCount > 0) messages.push(`${updatedCount}๊ฐ ์์ `);
if (deletedCount > 0) messages.push(`${deletedCount}๊ฐ ์ญ์ `);
- if (messages.length > 0) {
- toast.success(`ํ๋ชฉ์ด ์ ์ฅ๋์์ต๋๋ค (${messages.join(", ")})`);
+ if (messages.length > 0 || hasRepeaterInstances) {
+ toast.success(messages.length > 0 ? `ํ๋ชฉ์ด ์ ์ฅ๋์์ต๋๋ค (${messages.join(", ")})` : "์ ์ฅ๋์์ต๋๋ค.");
// ๋ถ๋ชจ ์ปดํฌ๋ํธ์ onSave ์ฝ๋ฐฑ ์คํ (ํ
์ด๋ธ ์๋ก๊ณ ์นจ)
if (modalState.onSave) {
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx
index 05d8bdc9..252f5c2b 100644
--- a/frontend/components/screen/InteractiveScreenViewer.tsx
+++ b/frontend/components/screen/InteractiveScreenViewer.tsx
@@ -2231,11 +2231,20 @@ export const InteractiveScreenViewer: React.FC = (
}
: component;
- // ๐ ๋ชจ๋ ๋ ์ด์ด์ ์ปดํฌ๋ํธ๋ฅผ ํตํฉ (์กฐ๊ฑด๋ถ ๋ ์ด์ด ๋ด ์ปดํฌ๋ํธ๊ฐ ๊ธฐ๋ณธ ๋ ์ด์ด formData ์ฐธ์กฐ ๊ฐ๋ฅํ๋๋ก)
+ // ๋ชจ๋ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ํตํฉ (์กฐ๊ฑด ํ๊ฐ์ฉ - ํธ๋ฆฌ๊ฑฐ ์ปดํฌ๋ํธ ๊ฒ์์ ํ์)
const allLayerComponents = useMemo(() => {
return layers.flatMap((layer) => layer.components);
}, [layers]);
+ // ๐ง ํ์ฑ ๋ ์ด์ด ์ปดํฌ๋ํธ๋ง ํตํฉ (์ ์ฅ/๋ฐ์ดํฐ ์์ง์ฉ)
+ // ๊ธฐ๋ณธ ๋ ์ด์ด(base) + ํ์ฌ ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด๋ง ํฌํจ
+ // ๋นํ์ฑ ๋ ์ด์ด์ ์ค๋ณต columnName ์ปดํฌ๋ํธ๊ฐ ์ ์ฅ ๋ฐ์ดํฐ๋ฅผ ์ค์ผ์ํค๋ ๋ฌธ์ ํด๊ฒฐ
+ const visibleLayerComponents = useMemo(() => {
+ return layers
+ .filter((layer) => layer.type === "base" || activeLayerIds.includes(layer.id))
+ .flatMap((layer) => layer.components);
+ }, [layers, activeLayerIds]);
+
// ๐ ๋ ์ด์ด๋ณ ์ปดํฌ๋ํธ ๋ ๋๋ง ํจ์
const renderLayerComponents = useCallback((layer: LayerDefinition) => {
// ํ์ฑํ๋์ง ์์ ๋ ์ด์ด๋ ๋ ๋๋งํ์ง ์์
@@ -2272,7 +2281,7 @@ export const InteractiveScreenViewer: React.FC = (
>
= (
>
= (
>
= (
>
= (
})}
);
- }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, allLayerComponents, layers]);
+ }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, visibleLayerComponents, layers]);
return (
@@ -2485,7 +2494,13 @@ export const InteractiveScreenViewer: React.FC = (
setPopupScreen(null);
setPopupFormData({}); // ํ์
๋ซ์ ๋ formData๋ ์ด๊ธฐํ
}}>
-
+
{popupScreen?.title || "์์ธ ์ ๋ณด"}
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx
index 76bd8973..8dfe9ae4 100644
--- a/frontend/components/screen/ScreenDesigner.tsx
+++ b/frontend/components/screen/ScreenDesigner.tsx
@@ -2062,6 +2062,7 @@ export default function ScreenDesigner({
await screenApi.saveLayoutV2(selectedScreen.screenId, {
...v2Layout,
layerId: currentLayerId,
+ mainTableName: currentMainTableName, // ํ๋ฉด์ ๊ธฐ๋ณธ ํ
์ด๋ธ (DB ์
๋ฐ์ดํธ์ฉ)
});
} else {
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
@@ -5555,8 +5556,12 @@ export default function ScreenDesigner({
return false;
}
- // 6. ์ญ์ (๋จ์ผ/๋ค์ค ์ ํ ์ง์)
- if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) {
+ // 6. ์ญ์ (๋จ์ผ/๋ค์ค ์ ํ ์ง์) - Delete ๋๋ Backspace(Mac)
+ const isInputFocused = document.activeElement instanceof HTMLInputElement ||
+ document.activeElement instanceof HTMLTextAreaElement ||
+ document.activeElement instanceof HTMLSelectElement ||
+ (document.activeElement as HTMLElement)?.isContentEditable;
+ if ((e.key === "Delete" || (e.key === "Backspace" && !isInputFocused)) && (selectedComponent || groupState.selectedComponents.length > 0)) {
// console.log("๐๏ธ ์ปดํฌ๋ํธ ์ญ์ (๋จ์ถํค)");
e.preventDefault();
e.stopPropagation();
@@ -7418,7 +7423,7 @@ export default function ScreenDesigner({
ํธ์ง: Ctrl+C(๋ณต์ฌ), Ctrl+V(๋ถ์ฌ๋ฃ๊ธฐ), Ctrl+S(์ ์ฅ),
- Ctrl+Z(์คํ์ทจ์), Delete(์ญ์ )
+ Ctrl+Z(์คํ์ทจ์), Delete/Backspace(์ญ์ )
โ ๏ธ
diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx
index a076b867..aa7b894d 100644
--- a/frontend/components/screen/panels/ComponentsPanel.tsx
+++ b/frontend/components/screen/panels/ComponentsPanel.tsx
@@ -114,8 +114,7 @@ export function ComponentsPanel({
"image-display", // โ v2-media (image)
// ๊ณตํต์ฝ๋๊ด๋ฆฌ๋ก ํตํฉ ์์
"category-manager", // โ ๊ณตํต์ฝ๋๊ด๋ฆฌ ๊ธฐ๋ฅ์ผ๋ก ํตํฉ ์์
- // ๋ถํ ํจ๋ ์ ๋ฆฌ (split-panel-layout v1 ์ ์ง)
- "split-panel-layout2", // โ split-panel-layout๋ก ํตํฉ
+ // ๋ถํ ํจ๋ ์ ๋ฆฌ
"screen-split-panel", // ํ๋ฉด ์๋ฒ ๋ฉ ๋ฐฉ์์ ์ฌ์ฉํ์ง ์์
// ๋ฏธ์์ฑ/๋ฏธ์ฌ์ฉ ์ปดํฌ๋ํธ (๊ธฐ์กด ํ๋ฉด ํธํ์ฑ ์ ์ง, ์ ์ถ๊ฐ๋ง ๋ง์)
"accordion-basic", // ์์ฝ๋์ธ ์ปดํฌ๋ํธ
diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx
index 12dcc19a..3cbae41e 100644
--- a/frontend/components/screen/panels/TablesPanel.tsx
+++ b/frontend/components/screen/panels/TablesPanel.tsx
@@ -44,6 +44,11 @@ interface EntityJoinTable {
tableName: string;
currentDisplayColumn: string;
availableColumns: EntityJoinColumn[];
+ // ๊ฐ์ ํ
์ด๋ธ์ด ์ฌ๋ฌ FK๋ก ์กฐ์ธ๋ ์ ์์ผ๋ฏ๋ก ์์ค ์ปฌ๋ผ์ผ๋ก ๊ตฌ๋ถ
+ joinConfig?: {
+ sourceColumn: string;
+ [key: string]: unknown;
+ };
}
interface TablesPanelProps {
@@ -414,7 +419,11 @@ export const TablesPanel: React.FC = ({
- {entityJoinTables.map((joinTable) => {
+ {entityJoinTables.map((joinTable, idx) => {
+ // ๊ฐ์ ํ
์ด๋ธ์ด ์ฌ๋ฌ FK๋ก ์กฐ์ธ๋ ์ ์์ผ๋ฏ๋ก sourceColumn์ผ๋ก ๊ณ ์ ํค ์์ฑ
+ const uniqueKey = joinTable.joinConfig?.sourceColumn
+ ? `entity-join-${joinTable.tableName}-${joinTable.joinConfig.sourceColumn}`
+ : `entity-join-${joinTable.tableName}-${idx}`;
const isExpanded = expandedJoinTables.has(joinTable.tableName);
// ๊ฒ์์ด๋ก ํํฐ๋ง
const filteredColumns = searchTerm
@@ -431,8 +440,7 @@ export const TablesPanel: React.FC = ({
}
return (
- // ์ํฐํฐ ์กฐ์ธ ํ
์ด๋ธ์ ๊ณ ์ ์ ๋์ฌ ์ถ๊ฐ (๋ฉ์ธ ํ
์ด๋ธ๊ณผ ํค ์ค๋ณต ๋ฐฉ์ง)
-
+
{/* ์กฐ์ธ ํ
์ด๋ธ ํค๋ */}
>({});
const [screenLoadingStates, setScreenLoadingStates] = useState
>({});
const [screenErrors, setScreenErrors] = useState>({});
- // ํญ๋ณ ํ๋ฉด ์ ๋ณด (screenId, tableName) ์ ์ฅ
- const [screenInfoMap, setScreenInfoMap] = useState>({});
+ // ํญ๋ณ ํ๋ฉด ์ ๋ณด (screenId, tableName) - ์ธ๋ผ์ธ ์ปดํฌ๋ํธ์ ํ
์ด๋ธ ์ค์ ์์ ์ถ์ถ
+ const screenInfoMap = React.useMemo(() => {
+ const map: Record = {};
+ for (const tab of tabs as ExtendedTabItem[]) {
+ const inlineComponents = tab.components || [];
+ if (inlineComponents.length > 0) {
+ // ์ธ๋ผ์ธ ์ปดํฌ๋ํธ์์ ํ
์ด๋ธ ์ปดํฌ๋ํธ์ selectedTable ์ถ์ถ
+ const tableComp = inlineComponents.find(
+ (c) => c.componentType === "v2-table-list" || c.componentType === "table-list",
+ );
+ const selectedTable = tableComp?.componentConfig?.selectedTable;
+ if (selectedTable || tab.screenId) {
+ map[tab.id] = {
+ id: tab.screenId,
+ tableName: selectedTable,
+ };
+ }
+ }
+ }
+ return map;
+ }, [tabs]);
// ์ปดํฌ๋ํธ ํญ ๋ชฉ๋ก ๋ณ๊ฒฝ ์ ๋๊ธฐํ
useEffect(() => {
@@ -157,21 +176,10 @@ export function TabsWidget({
) {
setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true }));
try {
- // ๋ ์ด์์๊ณผ ํ๋ฉด ์ ๋ณด๋ฅผ ๋ณ๋ ฌ๋ก ๋ก๋
- const [layoutData, screenDef] = await Promise.all([
- screenApi.getLayout(extTab.screenId),
- screenApi.getScreen(extTab.screenId),
- ]);
+ const layoutData = await screenApi.getLayout(extTab.screenId);
if (layoutData && layoutData.components) {
setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components }));
}
- // ํญ์ ํ๋ฉด ์ ๋ณด ์ ์ฅ (tableName ํฌํจ)
- if (screenDef) {
- setScreenInfoMap((prev) => ({
- ...prev,
- [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName },
- }));
- }
} catch (error) {
console.error(`ํญ "${tab.label}" ํ๋ฉด ๋ก๋ ์คํจ:`, error);
setScreenErrors((prev) => ({ ...prev, [tab.id]: "ํ๋ฉด์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค." }));
@@ -185,31 +193,6 @@ export function TabsWidget({
loadScreenLayouts();
}, [visibleTabs, screenLayouts, screenLoadingStates]);
- // screenInfoMap์ด ์๋ ํญ์ ํ๋ฉด ์ ๋ณด ๋ณด์ถฉ ๋ก๋
- // screenId๊ฐ ์์ง๋ง screenInfoMap์ ์์ง ์๋ ํญ์ ํ๋ฉด ์ ๋ณด๋ฅผ ๋ก๋
- useEffect(() => {
- const loadMissingScreenInfo = async () => {
- for (const tab of visibleTabs) {
- const extTab = tab as ExtendedTabItem;
- // screenId๊ฐ ์๊ณ screenInfoMap์ ์์ง ์๋ ๊ฒฝ์ฐ ๋ก๋
- if (extTab.screenId && !screenInfoMap[tab.id]) {
- try {
- const screenDef = await screenApi.getScreen(extTab.screenId);
- if (screenDef) {
- setScreenInfoMap((prev) => ({
- ...prev,
- [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName },
- }));
- }
- } catch (error) {
- console.error(`ํญ "${tab.label}" ํ๋ฉด ์ ๋ณด ๋ก๋ ์คํจ:`, error);
- }
- }
- }
- };
- loadMissingScreenInfo();
- }, [visibleTabs, screenInfoMap]);
-
// ์ ํ๋ ํญ ๋ณ๊ฒฝ ์ localStorage์ ์ ์ฅ + ActiveTab Context ์
๋ฐ์ดํธ
useEffect(() => {
if (persistSelection && typeof window !== "undefined") {
diff --git a/frontend/components/v2/V2Media.tsx b/frontend/components/v2/V2Media.tsx
index 733d6657..0a4faaae 100644
--- a/frontend/components/v2/V2Media.tsx
+++ b/frontend/components/v2/V2Media.tsx
@@ -2,13 +2,13 @@
/**
* V2Media
- *
+ *
* ํตํฉ ๋ฏธ๋์ด ์ปดํฌ๋ํธ (๋ ๊ฑฐ์ FileUploadComponent ๊ธฐ๋ฅ ํตํฉ)
* - file: ํ์ผ ์
๋ก๋
* - image: ์ด๋ฏธ์ง ์
๋ก๋/ํ์
* - video: ๋น๋์ค
* - audio: ์ค๋์ค
- *
+ *
* ํต์ฌ ๊ธฐ๋ฅ:
* - FileViewerModal / FileManagerModal (์์ธํ๋ณด๊ธฐ)
* - ๋ํ ์ด๋ฏธ์ง ์ค์
@@ -24,9 +24,23 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { V2MediaProps } from "@/types/v2-components";
-import {
- Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2, Plus,
- FileText, Archive, Presentation, FileImage, FileVideo, FileAudio
+import {
+ Upload,
+ X,
+ File,
+ Image as ImageIcon,
+ Video,
+ Music,
+ Eye,
+ Download,
+ Trash2,
+ Plus,
+ FileText,
+ Archive,
+ Presentation,
+ FileImage,
+ FileVideo,
+ FileAudio,
} from "lucide-react";
import { apiClient } from "@/lib/api/client";
import { toast } from "sonner";
@@ -77,115 +91,276 @@ const getFileIcon = (extension: string) => {
/**
* V2 ๋ฏธ๋์ด ์ปดํฌ๋ํธ (๋ ๊ฑฐ์ ๊ธฐ๋ฅ ํตํฉ)
*/
-export const V2Media = forwardRef(
- (props, ref) => {
- const {
- id,
- label,
- required,
- readonly,
- disabled,
- style,
- size,
- config: configProp,
- value,
- onChange,
- formData,
- columnName,
- tableName,
- onFormDataChange,
- isDesignMode = false,
- isInteractive = true,
- onUpdate,
- ...restProps
- } = props;
+export const V2Media = forwardRef((props, ref) => {
+ const {
+ id,
+ label,
+ required,
+ readonly,
+ disabled,
+ style,
+ size,
+ config: configProp,
+ value,
+ onChange,
+ formData,
+ columnName,
+ tableName,
+ onFormDataChange,
+ isDesignMode = false,
+ isInteractive = true,
+ onUpdate,
+ ...restProps
+ } = props;
- // ์ธ์ฆ ์ ๋ณด
- const { user } = useAuth();
+ // ์ธ์ฆ ์ ๋ณด
+ const { user } = useAuth();
- // config ๊ธฐ๋ณธ๊ฐ
- const config = configProp || { type: "file" as const };
- const mediaType = config.type || "file";
+ // config ๊ธฐ๋ณธ๊ฐ
+ const config = configProp || { type: "file" as const };
+ const mediaType = config.type || "file";
- // ํ์ผ ์ํ
- const [uploadedFiles, setUploadedFiles] = useState([]);
- const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
- const [dragOver, setDragOver] = useState(false);
- const [representativeImageUrl, setRepresentativeImageUrl] = useState(null);
-
- // ๋ชจ๋ฌ ์ํ
- const [viewerFile, setViewerFile] = useState(null);
- const [isViewerOpen, setIsViewerOpen] = useState(false);
- const [isFileManagerOpen, setIsFileManagerOpen] = useState(false);
-
- const fileInputRef = useRef(null);
+ // ํ์ผ ์ํ
+ const [uploadedFiles, setUploadedFiles] = useState([]);
+ const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
+ const [dragOver, setDragOver] = useState(false);
+ const [representativeImageUrl, setRepresentativeImageUrl] = useState(null);
- // ๋ ์ฝ๋ ๋ชจ๋ ํ๋จ
- const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_'));
- const recordTableName = formData?.tableName || tableName;
- const recordId = formData?.id;
- // ๐ columnName ์ฐ์ ์ฌ์ฉ (์ค์ DB ์ปฌ๋ผ๋ช
), ์์ผ๋ฉด id, ์ตํ์ attachments
- const effectiveColumnName = columnName || id || 'attachments';
+ // ๋ชจ๋ฌ ์ํ
+ const [viewerFile, setViewerFile] = useState(null);
+ const [isViewerOpen, setIsViewerOpen] = useState(false);
+ const [isFileManagerOpen, setIsFileManagerOpen] = useState(false);
- // ๋ ์ฝ๋์ฉ targetObjid ์์ฑ
- const getRecordTargetObjid = useCallback(() => {
- if (isRecordMode && recordTableName && recordId) {
- return `${recordTableName}:${recordId}:${effectiveColumnName}`;
+ const fileInputRef = useRef(null);
+
+ // ๋ ์ฝ๋ ๋ชจ๋ ํ๋จ
+ const isRecordMode = !!(formData?.id && !String(formData.id).startsWith("temp_"));
+ const recordTableName = formData?.tableName || tableName;
+ const recordId = formData?.id;
+ // ๐ columnName ์ฐ์ ์ฌ์ฉ (์ค์ DB ์ปฌ๋ผ๋ช
), ์์ผ๋ฉด id, ์ตํ์ attachments
+ const effectiveColumnName = columnName || id || "attachments";
+
+ // ๋ ์ฝ๋์ฉ targetObjid ์์ฑ
+ const getRecordTargetObjid = useCallback(() => {
+ if (isRecordMode && recordTableName && recordId) {
+ return `${recordTableName}:${recordId}:${effectiveColumnName}`;
+ }
+ return null;
+ }, [isRecordMode, recordTableName, recordId, effectiveColumnName]);
+
+ // ๋ ์ฝ๋๋ณ ๊ณ ์ ํค ์์ฑ
+ const getUniqueKey = useCallback(() => {
+ if (isRecordMode && recordTableName && recordId) {
+ return `v2media_${recordTableName}_${recordId}_${id}`;
+ }
+ return `v2media_${id}`;
+ }, [isRecordMode, recordTableName, recordId, id]);
+
+ // ๋ ์ฝ๋ ID ๋ณ๊ฒฝ ์ ํ์ผ ๋ชฉ๋ก ์ด๊ธฐํ
+ const prevRecordIdRef = useRef(null);
+ useEffect(() => {
+ if (prevRecordIdRef.current !== recordId) {
+ prevRecordIdRef.current = recordId;
+ if (isRecordMode) {
+ setUploadedFiles([]);
}
- return null;
- }, [isRecordMode, recordTableName, recordId, effectiveColumnName]);
+ }
+ }, [recordId, isRecordMode]);
- // ๋ ์ฝ๋๋ณ ๊ณ ์ ํค ์์ฑ
- const getUniqueKey = useCallback(() => {
- if (isRecordMode && recordTableName && recordId) {
- return `v2media_${recordTableName}_${recordId}_${id}`;
- }
- return `v2media_${id}`;
- }, [isRecordMode, recordTableName, recordId, id]);
+ // ์ปดํฌ๋ํธ ๋ง์ดํธ ์ localStorage์์ ํ์ผ ๋ณต์
+ useEffect(() => {
+ if (!id) return;
- // ๋ ์ฝ๋ ID ๋ณ๊ฒฝ ์ ํ์ผ ๋ชฉ๋ก ์ด๊ธฐํ
- const prevRecordIdRef = useRef(null);
- useEffect(() => {
- if (prevRecordIdRef.current !== recordId) {
- prevRecordIdRef.current = recordId;
- if (isRecordMode) {
- setUploadedFiles([]);
- }
- }
- }, [recordId, isRecordMode]);
+ try {
+ const backupKey = getUniqueKey();
+ const backupFiles = localStorage.getItem(backupKey);
+ if (backupFiles) {
+ const parsedFiles = JSON.parse(backupFiles);
+ if (parsedFiles.length > 0) {
+ setUploadedFiles(parsedFiles);
- // ์ปดํฌ๋ํธ ๋ง์ดํธ ์ localStorage์์ ํ์ผ ๋ณต์
- useEffect(() => {
- if (!id) return;
-
- try {
- const backupKey = getUniqueKey();
- const backupFiles = localStorage.getItem(backupKey);
- if (backupFiles) {
- const parsedFiles = JSON.parse(backupFiles);
- if (parsedFiles.length > 0) {
- setUploadedFiles(parsedFiles);
-
- if (typeof window !== "undefined") {
- (window as any).globalFileState = {
- ...(window as any).globalFileState,
- [backupKey]: parsedFiles,
- };
- }
+ if (typeof window !== "undefined") {
+ (window as any).globalFileState = {
+ ...(window as any).globalFileState,
+ [backupKey]: parsedFiles,
+ };
}
}
- } catch (e) {
- console.warn("ํ์ผ ๋ณต์ ์คํจ:", e);
}
- }, [id, getUniqueKey, recordId]);
+ } catch (e) {
+ console.warn("ํ์ผ ๋ณต์ ์คํจ:", e);
+ }
+ }, [id, getUniqueKey, recordId]);
- // DB์์ ํ์ผ ๋ชฉ๋ก ๋ก๋
- const loadComponentFiles = useCallback(async () => {
- if (!id) return false;
+ // DB์์ ํ์ผ ๋ชฉ๋ก ๋ก๋
+ const loadComponentFiles = useCallback(async () => {
+ if (!id) return false;
+
+ try {
+ let screenId = formData?.screenId;
+
+ if (!screenId && typeof window !== "undefined") {
+ const pathname = window.location.pathname;
+ const screenMatch = pathname.match(/\/screens\/(\d+)/);
+ if (screenMatch) {
+ screenId = parseInt(screenMatch[1]);
+ }
+ }
+
+ if (!screenId && isDesignMode) {
+ screenId = 999999;
+ }
+
+ if (!screenId) {
+ screenId = 0;
+ }
+
+ const params = {
+ screenId,
+ componentId: id,
+ tableName: recordTableName || formData?.tableName || tableName,
+ recordId: recordId || formData?.id,
+ columnName: effectiveColumnName,
+ };
+
+ const response = await getComponentFiles(params);
+
+ if (response.success) {
+ const formattedFiles = response.totalFiles.map((file: any) => ({
+ objid: file.objid || file.id,
+ savedFileName: file.savedFileName || file.saved_file_name,
+ realFileName: file.realFileName || file.real_file_name,
+ fileSize: file.fileSize || file.file_size,
+ fileExt: file.fileExt || file.file_ext,
+ regdate: file.regdate,
+ status: file.status || "ACTIVE",
+ uploadedAt: file.uploadedAt || new Date().toISOString(),
+ targetObjid: file.targetObjid || file.target_objid,
+ filePath: file.filePath || file.file_path,
+ ...file,
+ }));
+
+ // localStorage์ ๋ณํฉ
+ let finalFiles = formattedFiles;
+ const uniqueKey = getUniqueKey();
+ try {
+ const backupFiles = localStorage.getItem(uniqueKey);
+ if (backupFiles) {
+ const parsedBackupFiles = JSON.parse(backupFiles);
+ const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid));
+ const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
+ finalFiles = [...formattedFiles, ...additionalFiles];
+ }
+ } catch (e) {
+ console.warn("ํ์ผ ๋ณํฉ ์ค๋ฅ:", e);
+ }
+
+ setUploadedFiles(finalFiles);
+
+ if (typeof window !== "undefined") {
+ (window as any).globalFileState = {
+ ...(window as any).globalFileState,
+ [uniqueKey]: finalFiles,
+ };
+
+ GlobalFileManager.registerFiles(finalFiles, {
+ uploadPage: window.location.pathname,
+ componentId: id,
+ screenId: formData?.screenId,
+ recordId: recordId,
+ });
+
+ try {
+ localStorage.setItem(uniqueKey, JSON.stringify(finalFiles));
+ } catch (e) {
+ console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
+ }
+ }
+ return true;
+ }
+ } catch (error) {
+ console.error("ํ์ผ ์กฐํ ์ค๋ฅ:", error);
+ }
+ return false;
+ }, [
+ id,
+ tableName,
+ columnName,
+ formData?.screenId,
+ formData?.tableName,
+ formData?.id,
+ getUniqueKey,
+ recordId,
+ isRecordMode,
+ recordTableName,
+ effectiveColumnName,
+ isDesignMode,
+ ]);
+
+ // ํ์ผ ๋๊ธฐํ
+ useEffect(() => {
+ loadComponentFiles();
+ }, [loadComponentFiles]);
+
+ // ์ ์ญ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
+ useEffect(() => {
+ const handleGlobalFileStateChange = (event: CustomEvent) => {
+ const { componentId, files, isRestore } = event.detail;
+
+ if (componentId === id) {
+ setUploadedFiles(files);
+
+ try {
+ const backupKey = getUniqueKey();
+ localStorage.setItem(backupKey, JSON.stringify(files));
+ } catch (e) {
+ console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
+ }
+ }
+ };
+
+ if (typeof window !== "undefined") {
+ window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
+ return () => {
+ window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
+ };
+ }
+ }, [id, getUniqueKey]);
+
+ // ํ์ผ ์
๋ก๋ ์ฒ๋ฆฌ
+ const handleFileUpload = useCallback(
+ async (files: File[]) => {
+ if (!files.length) return;
+
+ // ์ค๋ณต ์ฒดํฌ
+ const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase());
+ const duplicates: string[] = [];
+ const uniqueFiles: File[] = [];
+
+ files.forEach((file) => {
+ const fileName = file.name.toLowerCase();
+ if (existingFileNames.includes(fileName)) {
+ duplicates.push(file.name);
+ } else {
+ uniqueFiles.push(file);
+ }
+ });
+
+ if (duplicates.length > 0) {
+ toast.error(`์ค๋ณต๋ ํ์ผ: ${duplicates.join(", ")}`);
+ if (uniqueFiles.length === 0) return;
+ toast.info(`${uniqueFiles.length}๊ฐ์ ์๋ก์ด ํ์ผ๋ง ์
๋ก๋ํฉ๋๋ค.`);
+ }
+
+ const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
+ setUploadStatus("uploading");
+ toast.loading("ํ์ผ ์
๋ก๋ ์ค...", { id: "file-upload" });
try {
+ const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table";
+ const effectiveRecordId = recordId || formData?.id;
+
let screenId = formData?.screenId;
-
if (!screenId && typeof window !== "undefined") {
const pathname = window.location.pathname;
const screenMatch = pathname.match(/\/screens\/(\d+)/);
@@ -194,368 +369,73 @@ export const V2Media = forwardRef(
}
}
- if (!screenId && isDesignMode) {
- screenId = 999999;
+ let targetObjid;
+ const effectiveIsRecordMode =
+ isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith("temp_"));
+
+ if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
+ targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
+ } else if (screenId) {
+ targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`;
+ } else {
+ targetObjid = `temp_${id}`;
}
- if (!screenId) {
- screenId = 0;
- }
+ const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
- const params = {
- screenId,
- componentId: id,
- tableName: recordTableName || formData?.tableName || tableName,
- recordId: recordId || formData?.id,
+ const finalLinkedTable = effectiveIsRecordMode
+ ? effectiveTableName
+ : formData?.linkedTable || effectiveTableName;
+
+ const uploadData = {
+ autoLink: formData?.autoLink || true,
+ linkedTable: finalLinkedTable,
+ recordId: effectiveRecordId || `temp_${id}`,
columnName: effectiveColumnName,
+ isVirtualFileColumn: formData?.isVirtualFileColumn || true,
+ docType: config?.docType || "DOCUMENT",
+ docTypeName: config?.docTypeName || "์ผ๋ฐ ๋ฌธ์",
+ companyCode: userCompanyCode,
+ tableName: effectiveTableName,
+ fieldName: effectiveColumnName,
+ targetObjid: targetObjid,
+ isRecordMode: effectiveIsRecordMode,
};
- const response = await getComponentFiles(params);
+ const response = await uploadFiles({
+ files: filesToUpload,
+ ...uploadData,
+ });
if (response.success) {
- const formattedFiles = response.totalFiles.map((file: any) => ({
+ const fileData = response.files || (response as any).data || [];
+
+ if (fileData.length === 0) {
+ throw new Error("์
๋ก๋๋ ํ์ผ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ง ๋ชปํ์ต๋๋ค.");
+ }
+
+ const newFiles = fileData.map((file: any) => ({
objid: file.objid || file.id,
- savedFileName: file.savedFileName || file.saved_file_name,
- realFileName: file.realFileName || file.real_file_name,
- fileSize: file.fileSize || file.file_size,
- fileExt: file.fileExt || file.file_ext,
+ savedFileName: file.saved_file_name || file.savedFileName,
+ realFileName: file.real_file_name || file.realFileName || file.name,
+ fileSize: file.file_size || file.fileSize || file.size,
+ fileExt: file.file_ext || file.fileExt || file.extension,
+ filePath: file.file_path || file.filePath || file.path,
+ docType: file.doc_type || file.docType,
+ docTypeName: file.doc_type_name || file.docTypeName,
+ targetObjid: file.target_objid || file.targetObjid,
+ parentTargetObjid: file.parent_target_objid || file.parentTargetObjid,
+ companyCode: file.company_code || file.companyCode,
+ writer: file.writer,
regdate: file.regdate,
status: file.status || "ACTIVE",
- uploadedAt: file.uploadedAt || new Date().toISOString(),
- targetObjid: file.targetObjid || file.target_objid,
- filePath: file.filePath || file.file_path,
+ uploadedAt: new Date().toISOString(),
...file,
}));
- // localStorage์ ๋ณํฉ
- let finalFiles = formattedFiles;
- const uniqueKey = getUniqueKey();
- try {
- const backupFiles = localStorage.getItem(uniqueKey);
- if (backupFiles) {
- const parsedBackupFiles = JSON.parse(backupFiles);
- const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid));
- const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
- finalFiles = [...formattedFiles, ...additionalFiles];
- }
- } catch (e) {
- console.warn("ํ์ผ ๋ณํฉ ์ค๋ฅ:", e);
- }
-
- setUploadedFiles(finalFiles);
-
- if (typeof window !== "undefined") {
- (window as any).globalFileState = {
- ...(window as any).globalFileState,
- [uniqueKey]: finalFiles,
- };
-
- GlobalFileManager.registerFiles(finalFiles, {
- uploadPage: window.location.pathname,
- componentId: id,
- screenId: formData?.screenId,
- recordId: recordId,
- });
-
- try {
- localStorage.setItem(uniqueKey, JSON.stringify(finalFiles));
- } catch (e) {
- console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
- }
- }
- return true;
- }
- } catch (error) {
- console.error("ํ์ผ ์กฐํ ์ค๋ฅ:", error);
- }
- return false;
- }, [id, tableName, columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, isDesignMode]);
-
- // ํ์ผ ๋๊ธฐํ
- useEffect(() => {
- loadComponentFiles();
- }, [loadComponentFiles]);
-
- // ์ ์ญ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
- useEffect(() => {
- const handleGlobalFileStateChange = (event: CustomEvent) => {
- const { componentId, files, isRestore } = event.detail;
-
- if (componentId === id) {
- setUploadedFiles(files);
-
- try {
- const backupKey = getUniqueKey();
- localStorage.setItem(backupKey, JSON.stringify(files));
- } catch (e) {
- console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
- }
- }
- };
-
- if (typeof window !== "undefined") {
- window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
- return () => {
- window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
- };
- }
- }, [id, getUniqueKey]);
-
- // ํ์ผ ์
๋ก๋ ์ฒ๋ฆฌ
- const handleFileUpload = useCallback(
- async (files: File[]) => {
- if (!files.length) return;
-
- // ์ค๋ณต ์ฒดํฌ
- const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase());
- const duplicates: string[] = [];
- const uniqueFiles: File[] = [];
-
- files.forEach((file) => {
- const fileName = file.name.toLowerCase();
- if (existingFileNames.includes(fileName)) {
- duplicates.push(file.name);
- } else {
- uniqueFiles.push(file);
- }
- });
-
- if (duplicates.length > 0) {
- toast.error(`์ค๋ณต๋ ํ์ผ: ${duplicates.join(", ")}`);
- if (uniqueFiles.length === 0) return;
- toast.info(`${uniqueFiles.length}๊ฐ์ ์๋ก์ด ํ์ผ๋ง ์
๋ก๋ํฉ๋๋ค.`);
- }
-
- const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
- setUploadStatus("uploading");
- toast.loading("ํ์ผ ์
๋ก๋ ์ค...", { id: "file-upload" });
-
- try {
- const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table";
- const effectiveRecordId = recordId || formData?.id;
-
- let screenId = formData?.screenId;
- if (!screenId && typeof window !== "undefined") {
- const pathname = window.location.pathname;
- const screenMatch = pathname.match(/\/screens\/(\d+)/);
- if (screenMatch) {
- screenId = parseInt(screenMatch[1]);
- }
- }
-
- let targetObjid;
- const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_'));
-
- if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
- targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
- } else if (screenId) {
- targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`;
- } else {
- targetObjid = `temp_${id}`;
- }
-
- const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
-
- const finalLinkedTable = effectiveIsRecordMode
- ? effectiveTableName
- : (formData?.linkedTable || effectiveTableName);
-
- const uploadData = {
- autoLink: formData?.autoLink || true,
- linkedTable: finalLinkedTable,
- recordId: effectiveRecordId || `temp_${id}`,
- columnName: effectiveColumnName,
- isVirtualFileColumn: formData?.isVirtualFileColumn || true,
- docType: config?.docType || "DOCUMENT",
- docTypeName: config?.docTypeName || "์ผ๋ฐ ๋ฌธ์",
- companyCode: userCompanyCode,
- tableName: effectiveTableName,
- fieldName: effectiveColumnName,
- targetObjid: targetObjid,
- isRecordMode: effectiveIsRecordMode,
- };
-
- const response = await uploadFiles({
- files: filesToUpload,
- ...uploadData,
- });
-
- if (response.success) {
- const fileData = response.files || (response as any).data || [];
-
- if (fileData.length === 0) {
- throw new Error("์
๋ก๋๋ ํ์ผ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ง ๋ชปํ์ต๋๋ค.");
- }
-
- const newFiles = fileData.map((file: any) => ({
- objid: file.objid || file.id,
- savedFileName: file.saved_file_name || file.savedFileName,
- realFileName: file.real_file_name || file.realFileName || file.name,
- fileSize: file.file_size || file.fileSize || file.size,
- fileExt: file.file_ext || file.fileExt || file.extension,
- filePath: file.file_path || file.filePath || file.path,
- docType: file.doc_type || file.docType,
- docTypeName: file.doc_type_name || file.docTypeName,
- targetObjid: file.target_objid || file.targetObjid,
- parentTargetObjid: file.parent_target_objid || file.parentTargetObjid,
- companyCode: file.company_code || file.companyCode,
- writer: file.writer,
- regdate: file.regdate,
- status: file.status || "ACTIVE",
- uploadedAt: new Date().toISOString(),
- ...file,
- }));
-
- const updatedFiles = [...uploadedFiles, ...newFiles];
- setUploadedFiles(updatedFiles);
- setUploadStatus("success");
-
- // localStorage ๋ฐฑ์
- try {
- const backupKey = getUniqueKey();
- localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
- } catch (e) {
- console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
- }
-
- // ์ ์ญ ์ํ ์
๋ฐ์ดํธ
- if (typeof window !== "undefined") {
- const globalFileState = (window as any).globalFileState || {};
- const uniqueKey = getUniqueKey();
- globalFileState[uniqueKey] = updatedFiles;
- (window as any).globalFileState = globalFileState;
-
- GlobalFileManager.registerFiles(newFiles, {
- uploadPage: window.location.pathname,
- componentId: id,
- screenId: formData?.screenId,
- recordId: recordId,
- });
-
- const syncEvent = new CustomEvent("globalFileStateChanged", {
- detail: {
- componentId: id,
- uniqueKey: uniqueKey,
- recordId: recordId,
- files: updatedFiles,
- fileCount: updatedFiles.length,
- timestamp: Date.now(),
- },
- });
- window.dispatchEvent(syncEvent);
- }
-
- // ๋ถ๋ชจ ์ปดํฌ๋ํธ ์
๋ฐ์ดํธ
- if (onUpdate) {
- onUpdate({
- uploadedFiles: updatedFiles,
- lastFileUpdate: Date.now(),
- });
- }
-
- // onChange ์ฝ๋ฐฑ (objid ๋ฐฐ์ด ๋๋ ๋จ์ผ ๊ฐ)
- const fileIds = updatedFiles.map((f) => f.objid);
- const finalValue = config.multiple ? fileIds : fileIds[0] || "";
- const targetColumn = columnName || effectiveColumnName;
-
- console.log("๐ค [V2Media] ํ์ผ ์
๋ก๋ ์๋ฃ - ๊ฐ ์ ๋ฌ:", {
- columnName: targetColumn,
- fileIds,
- finalValue,
- hasOnChange: !!onChange,
- hasOnFormDataChange: !!onFormDataChange,
- });
-
- if (onChange) {
- onChange(finalValue);
- }
-
- // ํผ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ - ๋ถ๋ชจ ์ปดํฌ๋ํธ ์๊ทธ๋์ฒ์ ๋ง๊ฒ (fieldName, value) ํ์
- if (onFormDataChange && targetColumn) {
- // ๐ ๋จ์ผ ํ์ผ: ์ฒซ ๋ฒ์งธ objid๋ง ์ ๋ฌ (DB ์ปฌ๋ผ์ ์ ์ฅ๋ ๊ฐ)
- // ๋ณต์ ํ์ผ: ์ฝค๋ง ๊ตฌ๋ถ ๋ฌธ์์ด๋ก ์ ๋ฌ
- const formValue = config.multiple
- ? fileIds.join(',')
- : (fileIds[0] || '');
-
- console.log("๐ [V2Media] formData ์
๋ฐ์ดํธ:", {
- columnName: targetColumn,
- fileIds,
- formValue,
- isMultiple: config.multiple,
- isRecordMode: effectiveIsRecordMode,
- });
- // (fieldName: string, value: any) ํ์์ผ๋ก ํธ์ถ
- onFormDataChange(targetColumn, formValue);
- }
-
- // ๊ทธ๋ฆฌ๋ ํ์ผ ์ํ ์๋ก๊ณ ์นจ ์ด๋ฒคํธ ๋ฐ์
- if (typeof window !== "undefined") {
- const refreshEvent = new CustomEvent("refreshFileStatus", {
- detail: {
- tableName: effectiveTableName,
- recordId: effectiveRecordId,
- columnName: targetColumn,
- targetObjid: targetObjid,
- fileCount: updatedFiles.length,
- },
- });
- window.dispatchEvent(refreshEvent);
- }
-
- toast.dismiss("file-upload");
- toast.success(`${newFiles.length}๊ฐ ํ์ผ ์
๋ก๋ ์๋ฃ`);
- } else {
- throw new Error(response.message || (response as any).error || "ํ์ผ ์
๋ก๋ ์คํจ");
- }
- } catch (error) {
- console.error("ํ์ผ ์
๋ก๋ ์ค๋ฅ:", error);
- setUploadStatus("error");
- toast.dismiss("file-upload");
- toast.error(`์
๋ก๋ ์ค๋ฅ: ${error instanceof Error ? error.message : "์ ์ ์๋ ์ค๋ฅ"}`);
- }
- },
- [config, uploadedFiles, onChange, id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, tableName, onUpdate, onFormDataChange, user, columnName],
- );
-
- // ํ์ผ ๋ทฐ์ด ์ด๊ธฐ/๋ซ๊ธฐ
- const handleFileView = useCallback((file: FileInfo) => {
- setViewerFile(file);
- setIsViewerOpen(true);
- }, []);
-
- const handleViewerClose = useCallback(() => {
- setIsViewerOpen(false);
- setViewerFile(null);
- }, []);
-
- // ํ์ผ ๋ค์ด๋ก๋
- const handleFileDownload = useCallback(async (file: FileInfo) => {
- try {
- await downloadFile({
- fileId: file.objid,
- serverFilename: file.savedFileName,
- originalName: file.realFileName,
- });
- toast.success(`${file.realFileName} ๋ค์ด๋ก๋ ์๋ฃ`);
- } catch (error) {
- console.error("ํ์ผ ๋ค์ด๋ก๋ ์ค๋ฅ:", error);
- toast.error("ํ์ผ ๋ค์ด๋ก๋ ์คํจ");
- }
- }, []);
-
- // ํ์ผ ์ญ์
- const handleFileDelete = useCallback(
- async (file: FileInfo | string) => {
- try {
- const fileId = typeof file === "string" ? file : file.objid;
- const fileName = typeof file === "string" ? "ํ์ผ" : file.realFileName;
- const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName;
-
- await deleteFile(fileId, serverFilename);
-
- const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId);
+ const updatedFiles = [...uploadedFiles, ...newFiles];
setUploadedFiles(updatedFiles);
+ setUploadStatus("success");
// localStorage ๋ฐฑ์
try {
@@ -572,6 +452,13 @@ export const V2Media = forwardRef(
globalFileState[uniqueKey] = updatedFiles;
(window as any).globalFileState = globalFileState;
+ GlobalFileManager.registerFiles(newFiles, {
+ uploadPage: window.location.pathname,
+ componentId: id,
+ screenId: formData?.screenId,
+ recordId: recordId,
+ });
+
const syncEvent = new CustomEvent("globalFileStateChanged", {
detail: {
componentId: id,
@@ -580,12 +467,12 @@ export const V2Media = forwardRef(
files: updatedFiles,
fileCount: updatedFiles.length,
timestamp: Date.now(),
- action: "delete",
},
});
window.dispatchEvent(syncEvent);
}
+ // ๋ถ๋ชจ ์ปดํฌ๋ํธ ์
๋ฐ์ดํธ
if (onUpdate) {
onUpdate({
uploadedFiles: updatedFiles,
@@ -593,15 +480,17 @@ export const V2Media = forwardRef(
});
}
- // onChange ์ฝ๋ฐฑ
+ // onChange ์ฝ๋ฐฑ (objid ๋ฐฐ์ด ๋๋ ๋จ์ผ ๊ฐ)
const fileIds = updatedFiles.map((f) => f.objid);
const finalValue = config.multiple ? fileIds : fileIds[0] || "";
const targetColumn = columnName || effectiveColumnName;
- console.log("๐๏ธ [V2Media] ํ์ผ ์ญ์ ์๋ฃ - ๊ฐ ์ ๋ฌ:", {
+ console.log("๐ค [V2Media] ํ์ผ ์
๋ก๋ ์๋ฃ - ๊ฐ ์ ๋ฌ:", {
columnName: targetColumn,
fileIds,
finalValue,
+ hasOnChange: !!onChange,
+ hasOnFormDataChange: !!onFormDataChange,
});
if (onChange) {
@@ -612,120 +501,286 @@ export const V2Media = forwardRef(
if (onFormDataChange && targetColumn) {
// ๐ ๋จ์ผ ํ์ผ: ์ฒซ ๋ฒ์งธ objid๋ง ์ ๋ฌ (DB ์ปฌ๋ผ์ ์ ์ฅ๋ ๊ฐ)
// ๋ณต์ ํ์ผ: ์ฝค๋ง ๊ตฌ๋ถ ๋ฌธ์์ด๋ก ์ ๋ฌ
- const formValue = config.multiple
- ? fileIds.join(',')
- : (fileIds[0] || '');
-
- console.log("๐๏ธ [V2Media] ์ญ์ ํ formData ์
๋ฐ์ดํธ:", {
+ const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || "";
+
+ console.log("๐ [V2Media] formData ์
๋ฐ์ดํธ:", {
columnName: targetColumn,
fileIds,
formValue,
+ isMultiple: config.multiple,
+ isRecordMode: effectiveIsRecordMode,
});
// (fieldName: string, value: any) ํ์์ผ๋ก ํธ์ถ
onFormDataChange(targetColumn, formValue);
}
- toast.success(`${fileName} ์ญ์ ์๋ฃ`);
- } catch (error) {
- console.error("ํ์ผ ์ญ์ ์ค๋ฅ:", error);
- toast.error("ํ์ผ ์ญ์ ์คํจ");
- }
- },
- [uploadedFiles, onUpdate, id, isRecordMode, onFormDataChange, recordTableName, recordId, effectiveColumnName, getUniqueKey, onChange, config.multiple, columnName],
- );
-
- // ๋ํ ์ด๋ฏธ์ง ๋ก๋
- const loadRepresentativeImage = useCallback(
- async (file: FileInfo) => {
- try {
- const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
- file.fileExt.toLowerCase().replace(".", "")
- );
-
- if (!isImage) {
- setRepresentativeImageUrl(null);
- return;
+ // ๊ทธ๋ฆฌ๋ ํ์ผ ์ํ ์๋ก๊ณ ์นจ ์ด๋ฒคํธ ๋ฐ์
+ if (typeof window !== "undefined") {
+ const refreshEvent = new CustomEvent("refreshFileStatus", {
+ detail: {
+ tableName: effectiveTableName,
+ recordId: effectiveRecordId,
+ columnName: targetColumn,
+ targetObjid: targetObjid,
+ fileCount: updatedFiles.length,
+ },
+ });
+ window.dispatchEvent(refreshEvent);
}
- if (!file.objid || file.objid === "0" || file.objid === "") {
- setRepresentativeImageUrl(null);
- return;
- }
-
- const response = await apiClient.get(`/files/download/${file.objid}`, {
- params: { serverFilename: file.savedFileName },
- responseType: "blob",
- });
-
- const blob = new Blob([response.data]);
- const url = window.URL.createObjectURL(blob);
-
- if (representativeImageUrl) {
- window.URL.revokeObjectURL(representativeImageUrl);
- }
-
- setRepresentativeImageUrl(url);
- } catch (error) {
- console.error("๋ํ ์ด๋ฏธ์ง ๋ก๋ ์คํจ:", error);
- setRepresentativeImageUrl(null);
+ toast.dismiss("file-upload");
+ toast.success(`${newFiles.length}๊ฐ ํ์ผ ์
๋ก๋ ์๋ฃ`);
+ } else {
+ throw new Error(response.message || (response as any).error || "ํ์ผ ์
๋ก๋ ์คํจ");
}
- },
- [representativeImageUrl],
- );
-
- // ๋ํ ์ด๋ฏธ์ง ์ค์
- const handleSetRepresentative = useCallback(
- async (file: FileInfo) => {
- try {
- const { setRepresentativeFile } = await import("@/lib/api/file");
- await setRepresentativeFile(file.objid);
-
- const updatedFiles = uploadedFiles.map((f) => ({
- ...f,
- isRepresentative: f.objid === file.objid,
- }));
-
- setUploadedFiles(updatedFiles);
- loadRepresentativeImage(file);
- } catch (e) {
- console.error("๋ํ ํ์ผ ์ค์ ์คํจ:", e);
- }
- },
- [uploadedFiles, loadRepresentativeImage]
- );
-
- // uploadedFiles ๋ณ๊ฒฝ ์ ๋ํ ์ด๋ฏธ์ง ๋ก๋
- useEffect(() => {
- const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0];
- if (representativeFile) {
- loadRepresentativeImage(representativeFile);
- } else {
- setRepresentativeImageUrl(null);
+ } catch (error) {
+ console.error("ํ์ผ ์
๋ก๋ ์ค๋ฅ:", error);
+ setUploadStatus("error");
+ toast.dismiss("file-upload");
+ toast.error(`์
๋ก๋ ์ค๋ฅ: ${error instanceof Error ? error.message : "์ ์ ์๋ ์ค๋ฅ"}`);
}
+ },
+ [
+ config,
+ uploadedFiles,
+ onChange,
+ id,
+ getUniqueKey,
+ recordId,
+ isRecordMode,
+ recordTableName,
+ effectiveColumnName,
+ tableName,
+ onUpdate,
+ onFormDataChange,
+ user,
+ columnName,
+ ],
+ );
+
+ // ํ์ผ ๋ทฐ์ด ์ด๊ธฐ/๋ซ๊ธฐ
+ const handleFileView = useCallback((file: FileInfo) => {
+ setViewerFile(file);
+ setIsViewerOpen(true);
+ }, []);
+
+ const handleViewerClose = useCallback(() => {
+ setIsViewerOpen(false);
+ setViewerFile(null);
+ }, []);
+
+ // ํ์ผ ๋ค์ด๋ก๋
+ const handleFileDownload = useCallback(async (file: FileInfo) => {
+ try {
+ await downloadFile({
+ fileId: file.objid,
+ serverFilename: file.savedFileName,
+ originalName: file.realFileName,
+ });
+ toast.success(`${file.realFileName} ๋ค์ด๋ก๋ ์๋ฃ`);
+ } catch (error) {
+ console.error("ํ์ผ ๋ค์ด๋ก๋ ์ค๋ฅ:", error);
+ toast.error("ํ์ผ ๋ค์ด๋ก๋ ์คํจ");
+ }
+ }, []);
+
+ // ํ์ผ ์ญ์
+ const handleFileDelete = useCallback(
+ async (file: FileInfo | string) => {
+ try {
+ const fileId = typeof file === "string" ? file : file.objid;
+ const fileName = typeof file === "string" ? "ํ์ผ" : file.realFileName;
+ const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName;
+
+ await deleteFile(fileId, serverFilename);
+
+ const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId);
+ setUploadedFiles(updatedFiles);
+
+ // localStorage ๋ฐฑ์
+ try {
+ const backupKey = getUniqueKey();
+ localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
+ } catch (e) {
+ console.warn("localStorage ๋ฐฑ์
์คํจ:", e);
+ }
+
+ // ์ ์ญ ์ํ ์
๋ฐ์ดํธ
+ if (typeof window !== "undefined") {
+ const globalFileState = (window as any).globalFileState || {};
+ const uniqueKey = getUniqueKey();
+ globalFileState[uniqueKey] = updatedFiles;
+ (window as any).globalFileState = globalFileState;
+
+ const syncEvent = new CustomEvent("globalFileStateChanged", {
+ detail: {
+ componentId: id,
+ uniqueKey: uniqueKey,
+ recordId: recordId,
+ files: updatedFiles,
+ fileCount: updatedFiles.length,
+ timestamp: Date.now(),
+ action: "delete",
+ },
+ });
+ window.dispatchEvent(syncEvent);
+ }
+
+ if (onUpdate) {
+ onUpdate({
+ uploadedFiles: updatedFiles,
+ lastFileUpdate: Date.now(),
+ });
+ }
+
+ // onChange ์ฝ๋ฐฑ
+ const fileIds = updatedFiles.map((f) => f.objid);
+ const finalValue = config.multiple ? fileIds : fileIds[0] || "";
+ const targetColumn = columnName || effectiveColumnName;
+
+ console.log("๐๏ธ [V2Media] ํ์ผ ์ญ์ ์๋ฃ - ๊ฐ ์ ๋ฌ:", {
+ columnName: targetColumn,
+ fileIds,
+ finalValue,
+ });
+
+ if (onChange) {
+ onChange(finalValue);
+ }
+
+ // ํผ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ - ๋ถ๋ชจ ์ปดํฌ๋ํธ ์๊ทธ๋์ฒ์ ๋ง๊ฒ (fieldName, value) ํ์
+ if (onFormDataChange && targetColumn) {
+ // ๐ ๋จ์ผ ํ์ผ: ์ฒซ ๋ฒ์งธ objid๋ง ์ ๋ฌ (DB ์ปฌ๋ผ์ ์ ์ฅ๋ ๊ฐ)
+ // ๋ณต์ ํ์ผ: ์ฝค๋ง ๊ตฌ๋ถ ๋ฌธ์์ด๋ก ์ ๋ฌ
+ const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || "";
+
+ console.log("๐๏ธ [V2Media] ์ญ์ ํ formData ์
๋ฐ์ดํธ:", {
+ columnName: targetColumn,
+ fileIds,
+ formValue,
+ });
+ // (fieldName: string, value: any) ํ์์ผ๋ก ํธ์ถ
+ onFormDataChange(targetColumn, formValue);
+ }
+
+ toast.success(`${fileName} ์ญ์ ์๋ฃ`);
+ } catch (error) {
+ console.error("ํ์ผ ์ญ์ ์ค๋ฅ:", error);
+ toast.error("ํ์ผ ์ญ์ ์คํจ");
+ }
+ },
+ [
+ uploadedFiles,
+ onUpdate,
+ id,
+ isRecordMode,
+ onFormDataChange,
+ recordTableName,
+ recordId,
+ effectiveColumnName,
+ getUniqueKey,
+ onChange,
+ config.multiple,
+ columnName,
+ ],
+ );
+
+ // ๋ํ ์ด๋ฏธ์ง ๋ก๋
+ const loadRepresentativeImage = useCallback(
+ async (file: FileInfo) => {
+ try {
+ const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
+ file.fileExt.toLowerCase().replace(".", ""),
+ );
+
+ if (!isImage) {
+ setRepresentativeImageUrl(null);
+ return;
+ }
+
+ if (!file.objid || file.objid === "0" || file.objid === "") {
+ setRepresentativeImageUrl(null);
+ return;
+ }
+
+ const response = await apiClient.get(`/files/download/${file.objid}`, {
+ params: { serverFilename: file.savedFileName },
+ responseType: "blob",
+ });
+
+ const blob = new Blob([response.data]);
+ const url = window.URL.createObjectURL(blob);
- return () => {
if (representativeImageUrl) {
window.URL.revokeObjectURL(representativeImageUrl);
}
- };
- }, [uploadedFiles]);
- // ๋๋๊ทธ ์ค ๋๋กญ ํธ๋ค๋ฌ
- const handleDragOver = useCallback((e: React.DragEvent) => {
+ setRepresentativeImageUrl(url);
+ } catch (error) {
+ console.error("๋ํ ์ด๋ฏธ์ง ๋ก๋ ์คํจ:", error);
+ setRepresentativeImageUrl(null);
+ }
+ },
+ [representativeImageUrl],
+ );
+
+ // ๋ํ ์ด๋ฏธ์ง ์ค์
+ const handleSetRepresentative = useCallback(
+ async (file: FileInfo) => {
+ try {
+ const { setRepresentativeFile } = await import("@/lib/api/file");
+ await setRepresentativeFile(file.objid);
+
+ const updatedFiles = uploadedFiles.map((f) => ({
+ ...f,
+ isRepresentative: f.objid === file.objid,
+ }));
+
+ setUploadedFiles(updatedFiles);
+ loadRepresentativeImage(file);
+ } catch (e) {
+ console.error("๋ํ ํ์ผ ์ค์ ์คํจ:", e);
+ }
+ },
+ [uploadedFiles, loadRepresentativeImage],
+ );
+
+ // uploadedFiles ๋ณ๊ฒฝ ์ ๋ํ ์ด๋ฏธ์ง ๋ก๋
+ useEffect(() => {
+ const representativeFile = uploadedFiles.find((f) => f.isRepresentative) || uploadedFiles[0];
+ if (representativeFile) {
+ loadRepresentativeImage(representativeFile);
+ } else {
+ setRepresentativeImageUrl(null);
+ }
+
+ return () => {
+ if (representativeImageUrl) {
+ window.URL.revokeObjectURL(representativeImageUrl);
+ }
+ };
+ }, [uploadedFiles]);
+
+ // ๋๋๊ทธ ์ค ๋๋กญ ํธ๋ค๋ฌ
+ const handleDragOver = useCallback(
+ (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!readonly && !disabled) {
setDragOver(true);
}
- }, [readonly, disabled]);
+ },
+ [readonly, disabled],
+ );
- const handleDragLeave = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setDragOver(false);
- }, []);
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDragOver(false);
+ }, []);
- const handleDrop = useCallback((e: React.DragEvent) => {
+ const handleDrop = useCallback(
+ (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragOver(false);
@@ -736,93 +791,93 @@ export const V2Media = forwardRef(
handleFileUpload(files);
}
}
- }, [readonly, disabled, handleFileUpload]);
+ },
+ [readonly, disabled, handleFileUpload],
+ );
- // ํ์ผ ์ ํ
- const handleFileSelect = useCallback(() => {
- if (fileInputRef.current) {
- fileInputRef.current.click();
- }
- }, []);
+ // ํ์ผ ์ ํ
+ const handleFileSelect = useCallback(() => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }, []);
- const handleInputChange = useCallback((e: React.ChangeEvent) => {
+ const handleInputChange = useCallback(
+ (e: React.ChangeEvent) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
handleFileUpload(files);
}
- e.target.value = '';
- }, [handleFileUpload]);
+ e.target.value = "";
+ },
+ [handleFileUpload],
+ );
- // ํ์ผ ์ค์
- const fileConfig: FileUploadConfig = {
- accept: config.accept || "*/*",
- multiple: config.multiple || false,
- maxSize: config.maxSize || 10 * 1024 * 1024,
- disabled: disabled,
- readonly: readonly,
- };
+ // ํ์ผ ์ค์
+ const fileConfig: FileUploadConfig = {
+ accept: config.accept || "*/*",
+ multiple: config.multiple || false,
+ maxSize: config.maxSize || 10 * 1024 * 1024,
+ disabled: disabled,
+ readonly: readonly,
+ };
- const showLabel = label && style?.labelDisplay !== false;
- const componentWidth = size?.width || style?.width;
- const componentHeight = size?.height || style?.height;
+ const showLabel = label && style?.labelDisplay !== false;
+ const componentWidth = size?.width || style?.width;
+ const componentHeight = size?.height || style?.height;
- return (
-
- {/* ๋ผ๋ฒจ */}
- {showLabel && (
-
- )}
-
- {/* ๋ฉ์ธ ์ปจํ
์ด๋ */}
-
+ {/* ๋ผ๋ฒจ */}
+ {showLabel && (
+