공차등록성공

This commit is contained in:
leeheejin 2025-12-01 15:23:07 +09:00
parent be2550885a
commit 8d2ec8e737
5 changed files with 751 additions and 39 deletions

View File

@ -22,9 +22,9 @@ router.use(authenticateToken);
// 폼 데이터 CRUD // 폼 데이터 CRUD
router.post("/save", saveFormData); // 기존 버전 (레거시 지원) router.post("/save", saveFormData); // 기존 버전 (레거시 지원)
router.post("/save-enhanced", saveFormDataEnhanced); // 개선된 버전 router.post("/save-enhanced", saveFormDataEnhanced); // 개선된 버전
router.put("/update-field", updateFieldValue); // 특정 필드만 업데이트 (다른 테이블 지원) - /:id 보다 먼저 선언!
router.put("/:id", updateFormData); router.put("/:id", updateFormData);
router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트 router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트
router.put("/update-field", updateFieldValue); // 특정 필드만 업데이트 (다른 테이블 지원)
router.delete("/:id", deleteFormData); router.delete("/:id", deleteFormData);
router.get("/:id", getFormData); router.get("/:id", getFormData);

View File

@ -1662,12 +1662,47 @@ export class DynamicFormService {
companyCode, companyCode,
}); });
// 멀티테넌시: company_code 조건 추가 (최고관리자는 제외) // 테이블 컬럼 정보 조회 (updated_by, updated_at 존재 여부 확인)
let whereClause = `"${keyField}" = $1`; const columnQuery = `
const params: any[] = [keyValue, updateValue, userId]; SELECT column_name
let paramIndex = 4; FROM information_schema.columns
WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code')
`;
const columnResult = await client.query(columnQuery, [tableName]);
const existingColumns = columnResult.rows.map((row: any) => row.column_name);
const hasUpdatedBy = existingColumns.includes('updated_by');
const hasUpdatedAt = existingColumns.includes('updated_at');
const hasCompanyCode = existingColumns.includes('company_code');
if (companyCode && companyCode !== "*") { console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", {
hasUpdatedBy,
hasUpdatedAt,
hasCompanyCode,
});
// 동적 SET 절 구성
let setClause = `"${updateField}" = $1`;
const params: any[] = [updateValue];
let paramIndex = 2;
if (hasUpdatedBy) {
setClause += `, updated_by = $${paramIndex}`;
params.push(userId);
paramIndex++;
}
if (hasUpdatedAt) {
setClause += `, updated_at = NOW()`;
}
// WHERE 절 구성
let whereClause = `"${keyField}" = $${paramIndex}`;
params.push(keyValue);
paramIndex++;
// 멀티테넌시: company_code 조건 추가 (최고관리자는 제외, 컬럼이 있는 경우만)
if (hasCompanyCode && companyCode && companyCode !== "*") {
whereClause += ` AND company_code = $${paramIndex}`; whereClause += ` AND company_code = $${paramIndex}`;
params.push(companyCode); params.push(companyCode);
paramIndex++; paramIndex++;
@ -1675,9 +1710,7 @@ export class DynamicFormService {
const sqlQuery = ` const sqlQuery = `
UPDATE "${tableName}" UPDATE "${tableName}"
SET "${updateField}" = $2, SET ${setClause}
updated_by = $3,
updated_at = NOW()
WHERE ${whereClause} WHERE ${whereClause}
`; `;

View File

@ -1774,6 +1774,255 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
/> />
</div> </div>
{/* 첫 번째 추가 테이블 설정 (위치정보와 함께 상태 변경) */}
<div className="mt-4 border-t pt-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="geolocation-update-field"> ( 1)</Label>
<p className="text-xs text-muted-foreground"> </p>
</div>
<Switch
id="geolocation-update-field"
checked={config.action?.geolocationUpdateField === true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationUpdateField", checked)}
/>
</div>
{config.action?.geolocationUpdateField && (
<div className="mt-3 space-y-3 rounded-md bg-amber-50 p-3 dark:bg-amber-950">
<div>
<Label> </Label>
<Select
value={config.action?.geolocationExtraTableName || ""}
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationExtraTableName", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.name} value={table.name} className="text-xs">
{table.label || table.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label> </Label>
<Input
placeholder="예: status"
value={config.action?.geolocationExtraField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label> </Label>
<Input
placeholder="예: active"
value={config.action?.geolocationExtraValue || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraValue", e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label> ( )</Label>
<Input
placeholder="예: id"
value={config.action?.geolocationExtraKeyField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraKeyField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label> </Label>
<Select
value={config.action?.geolocationExtraKeySourceField || ""}
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationExtraKeySourceField", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__userId__" className="text-xs font-medium text-blue-600">
🔑 ID
</SelectItem>
<SelectItem value="__companyCode__" className="text-xs font-medium text-blue-600">
🏢
</SelectItem>
<SelectItem value="__userName__" className="text-xs font-medium text-blue-600">
👤
</SelectItem>
{tableColumns.length > 0 && (
<>
<SelectItem value="__divider__" disabled className="text-xs text-muted-foreground">
</SelectItem>
{tableColumns.map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
</div>
</div>
)}
</div>
{/* 두 번째 추가 테이블 설정 */}
<div className="mt-4 border-t pt-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="geolocation-second-table"> ( 2)</Label>
<p className="text-xs text-muted-foreground"> </p>
</div>
<Switch
id="geolocation-second-table"
checked={config.action?.geolocationSecondTableEnabled === true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationSecondTableEnabled", checked)}
/>
</div>
{config.action?.geolocationSecondTableEnabled && (
<div className="mt-3 space-y-3 rounded-md bg-green-50 p-3 dark:bg-green-950">
<div className="grid grid-cols-2 gap-2">
<div>
<Label> </Label>
<Select
value={config.action?.geolocationSecondTableName || ""}
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondTableName", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.name} value={table.name} className="text-xs">
{table.label || table.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label> </Label>
<Select
value={config.action?.geolocationSecondMode || "update"}
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondMode", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="update" className="text-xs">UPDATE ( )</SelectItem>
<SelectItem value="insert" className="text-xs">INSERT ( )</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label>{config.action?.geolocationSecondMode === "insert" ? "저장할 필드" : "변경할 필드"}</Label>
<Input
placeholder="예: status"
value={config.action?.geolocationSecondField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label>{config.action?.geolocationSecondMode === "insert" ? "저장할 값" : "변경할 값"}</Label>
<Input
placeholder="예: inactive"
value={config.action?.geolocationSecondValue || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondValue", e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label>
{config.action?.geolocationSecondMode === "insert"
? "연결 필드 (대상 테이블)"
: "키 필드 (대상 테이블)"}
</Label>
<Input
placeholder={config.action?.geolocationSecondMode === "insert" ? "예: vehicle_id" : "예: id"}
value={config.action?.geolocationSecondKeyField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondKeyField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label> </Label>
<Select
value={config.action?.geolocationSecondKeySourceField || ""}
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondKeySourceField", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__userId__" className="text-xs font-medium text-blue-600">
🔑 ID
</SelectItem>
<SelectItem value="__companyCode__" className="text-xs font-medium text-blue-600">
🏢
</SelectItem>
<SelectItem value="__userName__" className="text-xs font-medium text-blue-600">
👤
</SelectItem>
{tableColumns.length > 0 && (
<>
<SelectItem value="__divider__" disabled className="text-xs text-muted-foreground">
</SelectItem>
{tableColumns.map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
</div>
{config.action?.geolocationSecondMode === "insert" && (
<div className="flex items-center justify-between rounded bg-green-100 p-2 dark:bg-green-900">
<div className="space-y-0.5">
<Label className="text-xs"> </Label>
<p className="text-[10px] text-muted-foreground">/ </p>
</div>
<Switch
checked={config.action?.geolocationSecondInsertFields?.includeLocation === true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationSecondInsertFields", {
...config.action?.geolocationSecondInsertFields,
includeLocation: checked
})}
/>
</div>
)}
<p className="text-[10px] text-green-700 dark:text-green-300">
{config.action?.geolocationSecondMode === "insert"
? "새 레코드를 생성합니다. 연결 필드로 현재 폼 데이터와 연결됩니다."
: "기존 레코드를 수정합니다. 키 필드로 레코드를 찾아 값을 변경합니다."}
</p>
</div>
)}
</div>
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950"> <div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
<p className="text-xs text-blue-900 dark:text-blue-100"> <p className="text-xs text-blue-900 dark:text-blue-100">
<strong> :</strong> <strong> :</strong>
@ -1784,6 +2033,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<br /> <br />
3. / 3. /
<br /> <br />
4.
<br />
<br />
<strong>:</strong> + vehicles.status를 inactive로
<br />
<br /> <br />
<strong>:</strong> HTTPS . <strong>:</strong> HTTPS .
</p> </p>
@ -1852,6 +2106,62 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div> </div>
</div> </div>
{/* 🆕 키 필드 설정 (레코드 식별용) */}
<div className="mt-4 border-t pt-4">
<h5 className="mb-3 text-xs font-medium text-muted-foreground"> </h5>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="update-key-field">
(DB ) <span className="text-destructive">*</span>
</Label>
<Input
id="update-key-field"
placeholder="예: user_id"
value={config.action?.updateKeyField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.updateKeyField", e.target.value)}
className="h-8 text-xs"
/>
<p className="mt-1 text-xs text-muted-foreground"> DB </p>
</div>
<div>
<Label htmlFor="update-key-source">
<span className="text-destructive">*</span>
</Label>
<Select
value={config.action?.updateKeySourceField || ""}
onValueChange={(value) => onUpdateProperty("componentConfig.action.updateKeySourceField", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="키 값 소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__userId__" className="text-xs">
<span className="flex items-center gap-1">
<span className="text-amber-500">🔑</span> ID
</span>
</SelectItem>
<SelectItem value="__userName__" className="text-xs">
<span className="flex items-center gap-1">
<span className="text-amber-500">🔑</span>
</span>
</SelectItem>
<SelectItem value="__companyCode__" className="text-xs">
<span className="flex items-center gap-1">
<span className="text-amber-500">🔑</span>
</span>
</SelectItem>
{tableColumns.map((column) => (
<SelectItem key={column} value={column} className="text-xs">
{column}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-xs text-muted-foreground"> </p>
</div>
</div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="update-auto-save"> </Label> <Label htmlFor="update-auto-save"> </Label>
@ -1899,15 +2209,78 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div> </div>
</div> </div>
{/* 위치정보 수집 옵션 */}
<div className="mt-4 border-t pt-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="update-with-geolocation"> </Label>
<p className="text-xs text-muted-foreground"> GPS </p>
</div>
<Switch
id="update-with-geolocation"
checked={config.action?.updateWithGeolocation === true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.updateWithGeolocation", checked)}
/>
</div>
{config.action?.updateWithGeolocation && (
<div className="mt-3 space-y-3 rounded-md bg-amber-50 p-3 dark:bg-amber-950">
<div className="grid grid-cols-2 gap-2">
<div>
<Label> <span className="text-destructive">*</span></Label>
<Input
placeholder="예: latitude"
value={config.action?.updateGeolocationLatField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.updateGeolocationLatField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label> <span className="text-destructive">*</span></Label>
<Input
placeholder="예: longitude"
value={config.action?.updateGeolocationLngField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.updateGeolocationLngField", e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label> ()</Label>
<Input
placeholder="예: accuracy"
value={config.action?.updateGeolocationAccuracyField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.updateGeolocationAccuracyField", e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label> ()</Label>
<Input
placeholder="예: location_time"
value={config.action?.updateGeolocationTimestampField || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.updateGeolocationTimestampField", e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<p className="text-[10px] text-amber-700 dark:text-amber-300">
GPS .
</p>
</div>
)}
</div>
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950"> <div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
<p className="text-xs text-blue-900 dark:text-blue-100"> <p className="text-xs text-blue-900 dark:text-blue-100">
<strong> :</strong> <strong> :</strong>
<br /> <br />
- 버튼: status &quot;active&quot; - 버튼: status&quot;active&quot; +
<br /> <br />
- 버튼: approval_status &quot;approved&quot; - 버튼: status를 &quot;inactive&quot; +
<br /> <br />
- 버튼: is_completed &quot;Y&quot; - 버튼: is_completed&quot;Y&quot;
</p> </p>
</div> </div>
</div> </div>

View File

@ -90,7 +90,7 @@ export function LocationSwapSelectorConfigPanel({
} }
}, [config?.dataSource?.tableName, config?.dataSource?.type]); }, [config?.dataSource?.tableName, config?.dataSource?.type]);
// 코드 카테고리 로드 // 코드 카테고리 로드 (API가 없을 수 있으므로 에러 무시)
useEffect(() => { useEffect(() => {
const loadCodeCategories = async () => { const loadCodeCategories = async () => {
try { try {
@ -103,8 +103,11 @@ export function LocationSwapSelectorConfigPanel({
})) }))
); );
} }
} catch (error) { } catch (error: any) {
console.error("코드 카테고리 로드 실패:", error); // 404는 API가 없는 것이므로 무시
if (error?.response?.status !== 404) {
console.error("코드 카테고리 로드 실패:", error);
}
} }
}; };
loadCodeCategories(); loadCodeCategories();
@ -368,14 +371,14 @@ export function LocationSwapSelectorConfigPanel({
<Label> ()</Label> <Label> ()</Label>
{tableColumns.length > 0 ? ( {tableColumns.length > 0 ? (
<Select <Select
value={config?.departureLabelField || ""} value={config?.departureLabelField || "__none__"}
onValueChange={(value) => handleChange("departureLabelField", value)} onValueChange={(value) => handleChange("departureLabelField", value === "__none__" ? "" : value)}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="컬럼 선택 (선택사항)" /> <SelectValue placeholder="컬럼 선택 (선택사항)" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value=""></SelectItem> <SelectItem value="__none__"></SelectItem>
{tableColumns.map((col) => ( {tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}> <SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName} {col.columnLabel || col.columnName}
@ -395,14 +398,14 @@ export function LocationSwapSelectorConfigPanel({
<Label> ()</Label> <Label> ()</Label>
{tableColumns.length > 0 ? ( {tableColumns.length > 0 ? (
<Select <Select
value={config?.destinationLabelField || ""} value={config?.destinationLabelField || "__none__"}
onValueChange={(value) => handleChange("destinationLabelField", value)} onValueChange={(value) => handleChange("destinationLabelField", value === "__none__" ? "" : value)}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="컬럼 선택 (선택사항)" /> <SelectValue placeholder="컬럼 선택 (선택사항)" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value=""></SelectItem> <SelectItem value="__none__"></SelectItem>
{tableColumns.map((col) => ( {tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}> <SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName} {col.columnLabel || col.columnName}

View File

@ -110,6 +110,16 @@ export interface ButtonActionConfig {
geolocationExtraValue?: string | number | boolean; // 추가로 변경할 값 (예: "active") geolocationExtraValue?: string | number | boolean; // 추가로 변경할 값 (예: "active")
geolocationExtraKeyField?: string; // 다른 테이블의 키 필드 (예: "vehicle_id") geolocationExtraKeyField?: string; // 다른 테이블의 키 필드 (예: "vehicle_id")
geolocationExtraKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") geolocationExtraKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id")
// 🆕 두 번째 테이블 설정 (위치정보 + 상태변경을 각각 다른 테이블에)
geolocationSecondTableEnabled?: boolean; // 두 번째 테이블 사용 여부
geolocationSecondTableName?: string; // 두 번째 테이블명 (예: "vehicles")
geolocationSecondMode?: "update" | "insert"; // 작업 모드 (기본: update)
geolocationSecondField?: string; // 두 번째 테이블에서 변경할 필드명 (예: "status")
geolocationSecondValue?: string | number | boolean; // 두 번째 테이블에서 변경할 값 (예: "inactive")
geolocationSecondKeyField?: string; // 두 번째 테이블의 키 필드 (예: "id") - UPDATE 모드에서만 사용
geolocationSecondKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") - UPDATE 모드에서만 사용
geolocationSecondInsertFields?: Record<string, any>; // INSERT 모드에서 추가로 넣을 필드들
// 필드 값 교환 관련 (출발지 ↔ 목적지) // 필드 값 교환 관련 (출발지 ↔ 목적지)
swapFieldA?: string; // 교환할 첫 번째 필드명 (예: "departure") swapFieldA?: string; // 교환할 첫 번째 필드명 (예: "departure")
@ -121,6 +131,13 @@ export interface ButtonActionConfig {
updateTargetValue?: string | number | boolean; // 변경할 값 (예: "active") updateTargetValue?: string | number | boolean; // 변경할 값 (예: "active")
updateAutoSave?: boolean; // 변경 후 자동 저장 여부 (기본: true) updateAutoSave?: boolean; // 변경 후 자동 저장 여부 (기본: true)
updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // 여러 필드 동시 변경 updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // 여러 필드 동시 변경
// 🆕 필드 값 변경 + 위치정보 수집 (update_field 액션에서 사용)
updateWithGeolocation?: boolean; // 위치정보도 함께 수집할지 여부
updateGeolocationLatField?: string; // 위도 저장 필드
updateGeolocationLngField?: string; // 경도 저장 필드
updateGeolocationAccuracyField?: string; // 정확도 저장 필드 (선택)
updateGeolocationTimestampField?: string; // 타임스탬프 저장 필드 (선택)
// 편집 관련 (수주관리 등 그룹별 다중 레코드 편집) // 편집 관련 (수주관리 등 그룹별 다중 레코드 편집)
editMode?: "modal" | "navigate" | "inline"; // 편집 모드 editMode?: "modal" | "navigate" | "inline"; // 편집 모드
@ -217,6 +234,44 @@ export interface ButtonActionContext {
componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정 componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정
} }
/**
* 🆕
* :
* - __userId__ : 로그인한 ID
* - __userName__ : 로그인한
* - __companyCode__ : 로그인한
* - __screenId__ : 현재 ID
* - __tableName__ : 현재
*/
export function resolveSpecialKeyword(
sourceField: string | undefined,
context: ButtonActionContext
): any {
if (!sourceField) return undefined;
// 특수 키워드 처리
switch (sourceField) {
case "__userId__":
console.log("🔑 특수 키워드 변환: __userId__ →", context.userId);
return context.userId;
case "__userName__":
console.log("🔑 특수 키워드 변환: __userName__ →", context.userName);
return context.userName;
case "__companyCode__":
console.log("🔑 특수 키워드 변환: __companyCode__ →", context.companyCode);
return context.companyCode;
case "__screenId__":
console.log("🔑 특수 키워드 변환: __screenId__ →", context.screenId);
return context.screenId;
case "__tableName__":
console.log("🔑 특수 키워드 변환: __tableName__ →", context.tableName);
return context.tableName;
default:
// 일반 폼 데이터에서 가져오기
return context.formData?.[sourceField];
}
}
/** /**
* *
*/ */
@ -3236,6 +3291,14 @@ export class ButtonActionExecutor {
private static async handleGeolocation(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> { private static async handleGeolocation(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try { try {
console.log("📍 위치정보 가져오기 액션 실행:", { config, context }); console.log("📍 위치정보 가져오기 액션 실행:", { config, context });
console.log("📍 [디버그] 추가 필드 설정값:", {
geolocationUpdateField: config.geolocationUpdateField,
geolocationExtraField: config.geolocationExtraField,
geolocationExtraValue: config.geolocationExtraValue,
geolocationExtraTableName: config.geolocationExtraTableName,
geolocationExtraKeyField: config.geolocationExtraKeyField,
geolocationExtraKeySourceField: config.geolocationExtraKeySourceField,
});
// 브라우저 Geolocation API 지원 확인 // 브라우저 Geolocation API 지원 확인
if (!navigator.geolocation) { if (!navigator.geolocation) {
@ -3296,26 +3359,35 @@ export class ButtonActionExecutor {
// 🆕 추가 필드 변경 (위치정보 + 상태변경) // 🆕 추가 필드 변경 (위치정보 + 상태변경)
let extraTableUpdated = false; let extraTableUpdated = false;
let secondTableUpdated = false;
if (config.geolocationUpdateField && config.geolocationExtraField && config.geolocationExtraValue !== undefined) { if (config.geolocationUpdateField && config.geolocationExtraField && config.geolocationExtraValue !== undefined) {
const extraTableName = config.geolocationExtraTableName; const extraTableName = config.geolocationExtraTableName || context.tableName; // 🆕 대상 테이블이 없으면 현재 테이블 사용
const currentTableName = config.geolocationTableName || context.tableName; const currentTableName = config.geolocationTableName || context.tableName;
const keySourceField = config.geolocationExtraKeySourceField;
// 다른 테이블에 UPDATE하는 경우 // 🆕 특수 키워드가 설정되어 있으면 바로 DB UPDATE (같은 테이블이어도)
if (extraTableName && extraTableName !== currentTableName) { const hasSpecialKeyword = keySourceField?.startsWith("__") && keySourceField?.endsWith("__");
console.log("📍 다른 테이블 필드 변경:", { const isDifferentTable = extraTableName && extraTableName !== currentTableName;
// 다른 테이블이거나 특수 키워드가 설정된 경우 → 바로 DB UPDATE
if (isDifferentTable || hasSpecialKeyword) {
console.log("📍 DB 직접 UPDATE:", {
targetTable: extraTableName, targetTable: extraTableName,
field: config.geolocationExtraField, field: config.geolocationExtraField,
value: config.geolocationExtraValue, value: config.geolocationExtraValue,
keyField: config.geolocationExtraKeyField, keyField: config.geolocationExtraKeyField,
keySourceField: config.geolocationExtraKeySourceField, keySourceField: keySourceField,
hasSpecialKeyword,
isDifferentTable,
}); });
// 키 값 가져오기 // 키 값 가져오기 (특수 키워드 지원)
const keyValue = context.formData?.[config.geolocationExtraKeySourceField || ""]; const keyValue = resolveSpecialKeyword(keySourceField, context);
if (keyValue && config.geolocationExtraKeyField) { if (keyValue && config.geolocationExtraKeyField) {
try { try {
// 다른 테이블 UPDATE API 호출 // DB UPDATE API 호출
const { apiClient } = await import("@/lib/api/client"); const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.put(`/dynamic-form/update-field`, { const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: extraTableName, tableName: extraTableName,
@ -3327,30 +3399,131 @@ export class ButtonActionExecutor {
if (response.data?.success) { if (response.data?.success) {
extraTableUpdated = true; extraTableUpdated = true;
console.log("✅ 다른 테이블 UPDATE 성공:", response.data); console.log("✅ DB UPDATE 성공:", response.data);
} else { } else {
console.error("❌ 다른 테이블 UPDATE 실패:", response.data); console.error("❌ DB UPDATE 실패:", response.data);
toast.error(`${extraTableName} 테이블 업데이트에 실패했습니다.`); toast.error(`${extraTableName} 테이블 업데이트에 실패했습니다.`);
} }
} catch (apiError) { } catch (apiError) {
console.error("❌ 다른 테이블 UPDATE API 오류:", apiError); console.error("❌ DB UPDATE API 오류:", apiError);
toast.error(`${extraTableName} 테이블 업데이트 중 오류가 발생했습니다.`); toast.error(`${extraTableName} 테이블 업데이트 중 오류가 발생했습니다.`);
} }
} else { } else {
console.warn("⚠️ 키 값이 없어서 다른 테이블 UPDATE를 건너뜁니다:", { console.warn("⚠️ 키 값이 없어서 DB UPDATE를 건너뜁니다:", {
keySourceField: config.geolocationExtraKeySourceField, keySourceField: keySourceField,
keyValue, keyValue,
}); });
} }
} else { } else {
// 같은 테이블 (현재 폼 데이터에 추가) // 같은 테이블이고 특수 키워드가 없는 경우 (현재 폼 데이터에 추가)
updates[config.geolocationExtraField] = config.geolocationExtraValue; updates[config.geolocationExtraField] = config.geolocationExtraValue;
console.log("📍 같은 테이블 추가 필드 변경:", { console.log("📍 같은 테이블 추가 필드 변경 (폼 데이터):", {
field: config.geolocationExtraField, field: config.geolocationExtraField,
value: config.geolocationExtraValue, value: config.geolocationExtraValue,
}); });
} }
} }
// 🆕 두 번째 테이블 INSERT 또는 UPDATE
if (config.geolocationSecondTableEnabled &&
config.geolocationSecondTableName) {
const secondMode = config.geolocationSecondMode || "update";
console.log("📍 두 번째 테이블 작업:", {
mode: secondMode,
targetTable: config.geolocationSecondTableName,
field: config.geolocationSecondField,
value: config.geolocationSecondValue,
keyField: config.geolocationSecondKeyField,
keySourceField: config.geolocationSecondKeySourceField,
});
try {
const { apiClient } = await import("@/lib/api/client");
if (secondMode === "insert") {
// INSERT 모드: 새 레코드 생성
const insertData: Record<string, any> = {
// 위치정보 포함 (선택적)
...(config.geolocationSecondInsertFields || {}),
};
// 기본 필드 추가
if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) {
insertData[config.geolocationSecondField] = config.geolocationSecondValue;
}
// 위치정보도 두 번째 테이블에 저장하려면 추가
// (선택적으로 위도/경도도 저장)
if (config.geolocationSecondInsertFields?.includeLocation) {
insertData[latField] = latitude;
insertData[lngField] = longitude;
if (config.geolocationAccuracyField) {
insertData[config.geolocationAccuracyField] = accuracy;
}
if (config.geolocationTimestampField) {
insertData[config.geolocationTimestampField] = timestamp.toISOString();
}
}
// 현재 폼에서 키 값 가져와서 연결 (외래키) - 특수 키워드 지원
if (config.geolocationSecondKeySourceField && config.geolocationSecondKeyField) {
const keyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context);
if (keyValue) {
insertData[config.geolocationSecondKeyField] = keyValue;
}
}
console.log("📍 두 번째 테이블 INSERT 데이터:", insertData);
const response = await apiClient.post(`/dynamic-form/save`, {
tableName: config.geolocationSecondTableName,
data: insertData,
});
if (response.data?.success) {
secondTableUpdated = true;
console.log("✅ 두 번째 테이블 INSERT 성공:", response.data);
} else {
console.error("❌ 두 번째 테이블 INSERT 실패:", response.data);
toast.error(`${config.geolocationSecondTableName} 테이블 저장에 실패했습니다.`);
}
} else {
// UPDATE 모드: 기존 레코드 수정
if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) {
// 특수 키워드 지원
const secondKeyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context);
if (secondKeyValue && config.geolocationSecondKeyField) {
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: config.geolocationSecondTableName,
keyField: config.geolocationSecondKeyField,
keyValue: secondKeyValue,
updateField: config.geolocationSecondField,
updateValue: config.geolocationSecondValue,
});
if (response.data?.success) {
secondTableUpdated = true;
console.log("✅ 두 번째 테이블 UPDATE 성공:", response.data);
} else {
console.error("❌ 두 번째 테이블 UPDATE 실패:", response.data);
toast.error(`${config.geolocationSecondTableName} 테이블 업데이트에 실패했습니다.`);
}
} else {
console.warn("⚠️ 두 번째 테이블 키 값이 없어서 UPDATE를 건너뜁니다:", {
keySourceField: config.geolocationSecondKeySourceField,
keyValue: secondKeyValue,
});
}
}
}
} catch (apiError) {
console.error("❌ 두 번째 테이블 API 오류:", apiError);
toast.error(`${config.geolocationSecondTableName} 테이블 작업 중 오류가 발생했습니다.`);
}
}
// formData 업데이트 // formData 업데이트
if (context.onFormDataChange) { if (context.onFormDataChange) {
@ -3371,6 +3544,11 @@ export class ButtonActionExecutor {
successMsg += `\n${config.geolocationExtraField}: ${config.geolocationExtraValue}`; successMsg += `\n${config.geolocationExtraField}: ${config.geolocationExtraValue}`;
} }
} }
// 두 번째 테이블 변경이 있으면 메시지에 포함
if (secondTableUpdated && config.geolocationSecondTableName) {
successMsg += `\n[${config.geolocationSecondTableName}] ${config.geolocationSecondField}: ${config.geolocationSecondValue}`;
}
// 성공 메시지 표시 // 성공 메시지 표시
toast.success(successMsg); toast.success(successMsg);
@ -3470,6 +3648,7 @@ export class ButtonActionExecutor {
/** /**
* (: status를 active로 ) * (: status를 active로 )
* 🆕
*/ */
private static async handleUpdateField(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> { private static async handleUpdateField(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try { try {
@ -3483,7 +3662,7 @@ export class ButtonActionExecutor {
const multipleFields = config.updateMultipleFields || []; const multipleFields = config.updateMultipleFields || [];
// 단일 필드 변경이나 다중 필드 변경 중 하나는 있어야 함 // 단일 필드 변경이나 다중 필드 변경 중 하나는 있어야 함
if (!targetField && multipleFields.length === 0) { if (!targetField && multipleFields.length === 0 && !config.updateWithGeolocation) {
toast.error("변경할 필드가 설정되지 않았습니다."); toast.error("변경할 필드가 설정되지 않았습니다.");
return false; return false;
} }
@ -3510,6 +3689,69 @@ export class ButtonActionExecutor {
updates[field] = value; updates[field] = value;
}); });
// 🆕 위치정보 수집 (updateWithGeolocation이 true인 경우)
if (config.updateWithGeolocation) {
const latField = config.updateGeolocationLatField;
const lngField = config.updateGeolocationLngField;
if (!latField || !lngField) {
toast.error("위도/경도 저장 필드가 설정되지 않았습니다.");
return false;
}
// 브라우저 Geolocation API 지원 확인
if (!navigator.geolocation) {
toast.error("이 브라우저는 위치정보를 지원하지 않습니다.");
return false;
}
// 로딩 토스트 표시
const loadingToastId = toast.loading("위치 정보를 가져오는 중...");
try {
// 위치 정보 가져오기
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
});
});
toast.dismiss(loadingToastId);
const { latitude, longitude, accuracy } = position.coords;
const timestamp = new Date(position.timestamp);
console.log("📍 위치정보 획득:", { latitude, longitude, accuracy });
// 위치정보를 updates에 추가
updates[latField] = latitude;
updates[lngField] = longitude;
if (config.updateGeolocationAccuracyField && accuracy !== null) {
updates[config.updateGeolocationAccuracyField] = accuracy;
}
if (config.updateGeolocationTimestampField) {
updates[config.updateGeolocationTimestampField] = timestamp.toISOString();
}
} catch (geoError: any) {
toast.dismiss(loadingToastId);
// GeolocationPositionError 처리
if (geoError.code === 1) {
toast.error("위치 정보 접근이 거부되었습니다.");
} else if (geoError.code === 2) {
toast.error("위치 정보를 사용할 수 없습니다.");
} else if (geoError.code === 3) {
toast.error("위치 정보 요청 시간이 초과되었습니다.");
} else {
toast.error("위치 정보를 가져오는 중 오류가 발생했습니다.");
}
return false;
}
}
console.log("🔄 변경할 필드들:", updates); console.log("🔄 변경할 필드들:", updates);
// formData 업데이트 // formData 업데이트
@ -3523,6 +3765,67 @@ export class ButtonActionExecutor {
const autoSave = config.updateAutoSave !== false; const autoSave = config.updateAutoSave !== false;
if (autoSave) { if (autoSave) {
// 🆕 키 필드 설정이 있는 경우 (특수 키워드 지원) - 직접 DB UPDATE
const keyField = config.updateKeyField;
const keySourceField = config.updateKeySourceField;
const targetTableName = config.updateTableName || tableName;
if (keyField && keySourceField) {
// 특수 키워드 변환 (예: __userId__ → 실제 사용자 ID)
const keyValue = resolveSpecialKeyword(keySourceField, context);
console.log("🔄 필드 값 변경 - 키 필드 사용:", {
targetTable: targetTableName,
keyField,
keySourceField,
keyValue,
updates,
});
if (!keyValue) {
console.warn("⚠️ 키 값이 없어서 업데이트를 건너뜁니다:", { keySourceField, keyValue });
toast.error("레코드를 식별할 키 값이 없습니다.");
return false;
}
try {
// 각 필드에 대해 개별 UPDATE 호출
const { apiClient } = await import("@/lib/api/client");
for (const [field, value] of Object.entries(updates)) {
console.log(`🔄 DB UPDATE: ${targetTableName}.${field} = ${value} WHERE ${keyField} = ${keyValue}`);
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: targetTableName,
keyField: keyField,
keyValue: keyValue,
updateField: field,
updateValue: value,
});
if (!response.data?.success) {
console.error(`${field} 업데이트 실패:`, response.data);
toast.error(`${field} 업데이트에 실패했습니다.`);
return false;
}
}
console.log("✅ 모든 필드 업데이트 성공");
toast.success(config.successMessage || "상태가 변경되었습니다.");
// 테이블 새로고침 이벤트 발생
window.dispatchEvent(new CustomEvent("refreshTableData", {
detail: { tableName: targetTableName }
}));
return true;
} catch (apiError) {
console.error("❌ 필드 값 변경 API 호출 실패:", apiError);
toast.error(config.errorMessage || "상태 변경 중 오류가 발생했습니다.");
return false;
}
}
// onSave 콜백이 있으면 사용 // onSave 콜백이 있으면 사용
if (onSave) { if (onSave) {
console.log("🔄 필드 값 변경 후 자동 저장 (onSave 콜백)"); console.log("🔄 필드 값 변경 후 자동 저장 (onSave 콜백)");
@ -3537,7 +3840,7 @@ export class ButtonActionExecutor {
} }
} }
// API를 통한 직접 저장 // API를 통한 직접 저장 (기존 방식: formData에 PK가 있는 경우)
if (tableName && formData) { if (tableName && formData) {
console.log("🔄 필드 값 변경 후 자동 저장 (API 직접 호출)"); console.log("🔄 필드 값 변경 후 자동 저장 (API 직접 호출)");
try { try {
@ -3546,7 +3849,7 @@ export class ButtonActionExecutor {
const pkValue = formData[pkField] || formData.id; const pkValue = formData[pkField] || formData.id;
if (!pkValue) { if (!pkValue) {
toast.error("레코드 ID를 찾을 수 없습니다."); toast.error("레코드 ID를 찾을 수 없습니다. 키 필드를 설정해주세요.");
return false; return false;
} }