Merge branch 'ksh-v2-work' into main
ksh-v2-work의 POP 화면 디자이너 기능을 main에 병합한다. [병합 내용] - pop-card-list-v2: 슬롯 기반 CSS Grid 카드 컴포넌트 (12종 셀 타입) - pop-status-bar: 독립 상태 칩 컴포넌트 (카운트 순환 문제 해결) - pop-scanner: 바코드/QR 스캐너 + 멀티필드 파싱 - pop-profile: 사용자 프로필/PC전환/로그아웃 컴포넌트 - pop-button: 설정 패널 UX 전면 개선 + 제어 실행 기능 - pop-search: 날짜 입력 타입 + 연결 탭 일관성 통합 - POP 모드 네비게이션: PC <-> POP 양방향 전환 + 로그인 POP 모드 토글 - 타임라인 범용화 + 상태 값 매핑 동적 배열 전환 - 다중 액션 체이닝 + 외부 테이블 선택 + 카드 클릭 모달 [충돌 해결 4건] - authController.ts: 양쪽 통합 (스마트공장 로그 + POP 랜딩 경로) - AppLayout.tsx: 양쪽 통합 (메뉴 드래그 + POP 모드 메뉴, 리디자인 UI + POP 모드 항목) - ConnectionEditor.tsx: ksh-v2-work 선택 (하위 테이블 필터 구조) + CSS 변수 적용 - pop-button.tsx: ksh-v2-work 선택 (자연어 UX + 제어 실행) + CSS 변수 스타일 유지
This commit is contained in:
@@ -322,7 +322,9 @@ export async function executeTaskList(
|
||||
}
|
||||
|
||||
case "custom-event":
|
||||
if (task.eventName) {
|
||||
if (task.flowId) {
|
||||
await apiClient.post(`/dataflow/node-flows/${task.flowId}/execute`, {});
|
||||
} else if (task.eventName) {
|
||||
publish(task.eventName, task.eventPayload ?? {});
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -20,7 +20,6 @@ import { usePopEvent } from "./usePopEvent";
|
||||
import type { PopDataConnection } from "@/components/pop/designer/types/pop-layout";
|
||||
import {
|
||||
PopComponentRegistry,
|
||||
type ConnectionMetaItem,
|
||||
} from "@/lib/registry/PopComponentRegistry";
|
||||
|
||||
interface UseConnectionResolverOptions {
|
||||
@@ -29,14 +28,21 @@ interface UseConnectionResolverOptions {
|
||||
componentTypes?: Map<string, string>;
|
||||
}
|
||||
|
||||
interface AutoMatchPair {
|
||||
sourceKey: string;
|
||||
targetKey: string;
|
||||
isFilter: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스/타겟의 connectionMeta에서 자동 매칭 가능한 이벤트 쌍을 찾는다.
|
||||
* 규칙: category="event"이고 key가 동일한 쌍
|
||||
* 소스/타겟의 connectionMeta에서 자동 매칭 가능한 쌍을 찾는다.
|
||||
* 규칙 1: category="event"이고 key가 동일한 쌍 (이벤트 매칭)
|
||||
* 규칙 2: 소스 type="filter_value" + 타겟 type="filter_value" (필터 매칭)
|
||||
*/
|
||||
function getAutoMatchPairs(
|
||||
sourceType: string,
|
||||
targetType: string
|
||||
): { sourceKey: string; targetKey: string }[] {
|
||||
): AutoMatchPair[] {
|
||||
const sourceDef = PopComponentRegistry.getComponent(sourceType);
|
||||
const targetDef = PopComponentRegistry.getComponent(targetType);
|
||||
|
||||
@@ -44,14 +50,18 @@ function getAutoMatchPairs(
|
||||
return [];
|
||||
}
|
||||
|
||||
const pairs: { sourceKey: string; targetKey: string }[] = [];
|
||||
const pairs: AutoMatchPair[] = [];
|
||||
|
||||
for (const s of sourceDef.connectionMeta.sendable) {
|
||||
if (s.category !== "event") continue;
|
||||
for (const r of targetDef.connectionMeta.receivable) {
|
||||
if (r.category !== "event") continue;
|
||||
if (s.key === r.key) {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key });
|
||||
if (s.category === "event" && r.category === "event" && s.key === r.key) {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: false });
|
||||
}
|
||||
if (s.type === "filter_value" && r.type === "filter_value") {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: true });
|
||||
}
|
||||
if (s.type === "all_rows" && r.type === "all_rows") {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,10 +103,30 @@ export function useConnectionResolver({
|
||||
const targetEvent = `__comp_input__${conn.targetComponent}__${pair.targetKey}`;
|
||||
|
||||
const unsub = subscribe(sourceEvent, (payload: unknown) => {
|
||||
publish(targetEvent, {
|
||||
value: payload,
|
||||
_connectionId: conn.id,
|
||||
});
|
||||
if (pair.isFilter) {
|
||||
const data = payload as Record<string, unknown> | null;
|
||||
const fieldName = data?.fieldName as string | undefined;
|
||||
const filterColumns = data?.filterColumns as string[] | undefined;
|
||||
const filterMode = (data?.filterMode as string) || "contains";
|
||||
// conn.filterConfig에 targetColumn이 명시되어 있으면 우선 사용
|
||||
const effectiveColumn = conn.filterConfig?.targetColumn || fieldName;
|
||||
const effectiveMode = conn.filterConfig?.filterMode || filterMode;
|
||||
const baseFilterConfig = effectiveColumn
|
||||
? { targetColumn: effectiveColumn, targetColumns: conn.filterConfig?.targetColumns || (filterColumns?.length ? filterColumns : [effectiveColumn]), filterMode: effectiveMode }
|
||||
: conn.filterConfig;
|
||||
publish(targetEvent, {
|
||||
value: payload,
|
||||
filterConfig: conn.filterConfig?.isSubTable
|
||||
? { ...baseFilterConfig, isSubTable: true }
|
||||
: baseFilterConfig,
|
||||
_connectionId: conn.id,
|
||||
});
|
||||
} else {
|
||||
publish(targetEvent, {
|
||||
value: payload,
|
||||
_connectionId: conn.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
unsubscribers.push(unsub);
|
||||
}
|
||||
@@ -121,13 +151,22 @@ export function useConnectionResolver({
|
||||
const unsub = subscribe(sourceEvent, (payload: unknown) => {
|
||||
const targetEvent = `__comp_input__${conn.targetComponent}__${conn.targetInput || conn.targetField}`;
|
||||
|
||||
const enrichedPayload = {
|
||||
value: payload,
|
||||
filterConfig: conn.filterConfig,
|
||||
_connectionId: conn.id,
|
||||
};
|
||||
let resolvedFilterConfig = conn.filterConfig;
|
||||
if (!resolvedFilterConfig) {
|
||||
const data = payload as Record<string, unknown> | null;
|
||||
const fieldName = data?.fieldName as string | undefined;
|
||||
const filterColumns = data?.filterColumns as string[] | undefined;
|
||||
if (fieldName) {
|
||||
const filterMode = (data?.filterMode as string) || "contains";
|
||||
resolvedFilterConfig = { targetColumn: fieldName, targetColumns: filterColumns?.length ? filterColumns : [fieldName], filterMode: filterMode as "equals" | "contains" | "starts_with" | "range" };
|
||||
}
|
||||
}
|
||||
|
||||
publish(targetEvent, enrichedPayload);
|
||||
publish(targetEvent, {
|
||||
value: payload,
|
||||
filterConfig: resolvedFilterConfig,
|
||||
_connectionId: conn.id,
|
||||
});
|
||||
});
|
||||
unsubscribers.push(unsub);
|
||||
}
|
||||
|
||||
+33
-11
@@ -20,6 +20,21 @@ export const useLogin = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isPopMode, setIsPopMode] = useState(false);
|
||||
|
||||
// localStorage에서 POP 모드 상태 복원
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("popLoginMode");
|
||||
if (saved === "true") setIsPopMode(true);
|
||||
}, []);
|
||||
|
||||
const togglePopMode = useCallback(() => {
|
||||
setIsPopMode((prev) => {
|
||||
const next = !prev;
|
||||
localStorage.setItem("popLoginMode", String(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 폼 입력값 변경 처리
|
||||
@@ -141,17 +156,22 @@ export const useLogin = () => {
|
||||
// 쿠키에도 저장 (미들웨어에서 사용)
|
||||
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
||||
|
||||
// 로그인 성공 - 첫 번째 접근 가능한 메뉴로 리다이렉트
|
||||
const firstMenuPath = result.data?.firstMenuPath;
|
||||
|
||||
if (firstMenuPath) {
|
||||
// 접근 가능한 메뉴가 있으면 해당 메뉴로 이동
|
||||
console.log("첫 번째 접근 가능한 메뉴로 이동:", firstMenuPath);
|
||||
router.push(firstMenuPath);
|
||||
if (isPopMode) {
|
||||
const popPath = result.data?.popLandingPath;
|
||||
if (popPath) {
|
||||
router.push(popPath);
|
||||
} else {
|
||||
setError("POP 화면이 설정되어 있지 않습니다. 관리자에게 메뉴 관리에서 POP 화면을 설정해달라고 요청하세요.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 접근 가능한 메뉴가 없으면 메인 페이지로 이동
|
||||
console.log("접근 가능한 메뉴가 없어 메인 페이지로 이동");
|
||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||
const firstMenuPath = result.data?.firstMenuPath;
|
||||
if (firstMenuPath) {
|
||||
router.push(firstMenuPath);
|
||||
} else {
|
||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 로그인 실패
|
||||
@@ -165,7 +185,7 @@ export const useLogin = () => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[formData, validateForm, apiCall, router],
|
||||
[formData, validateForm, apiCall, router, isPopMode],
|
||||
);
|
||||
|
||||
// 컴포넌트 마운트 시 기존 인증 상태 확인
|
||||
@@ -179,10 +199,12 @@ export const useLogin = () => {
|
||||
isLoading,
|
||||
error,
|
||||
showPassword,
|
||||
isPopMode,
|
||||
|
||||
// 액션
|
||||
handleInputChange,
|
||||
handleLogin,
|
||||
togglePasswordVisibility,
|
||||
togglePopMode,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user