수주등록 저장기능

This commit is contained in:
kjs 2025-11-20 15:30:00 +09:00
parent b46559ba78
commit 45ac397417
4 changed files with 107 additions and 47 deletions

View File

@ -31,7 +31,7 @@ interface ScreenModalProps {
} }
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => { export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const { userId } = useAuth(); const { userId, userName, user } = useAuth();
const [modalState, setModalState] = useState<ScreenModalState>({ const [modalState, setModalState] = useState<ScreenModalState>({
isOpen: false, isOpen: false,
@ -587,6 +587,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
id: modalState.screenId!, id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName, tableName: screenData.screenInfo?.tableName,
}} }}
userId={userId}
userName={userName}
companyCode={user?.companyCode}
/> />
); );
})} })}

View File

@ -42,6 +42,10 @@ interface InteractiveScreenViewerProps {
onSave?: () => Promise<void>; onSave?: () => Promise<void>;
onRefresh?: () => void; onRefresh?: () => void;
onFlowRefresh?: () => void; onFlowRefresh?: () => void;
// 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
userId?: string;
userName?: string;
companyCode?: string;
} }
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@ -54,9 +58,24 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
onSave, onSave,
onRefresh, onRefresh,
onFlowRefresh, onFlowRefresh,
userId: externalUserId,
userName: externalUserName,
companyCode: externalCompanyCode,
}) => { }) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName, user } = useAuth(); const { userName: authUserName, user: authUser } = useAuth();
// 외부에서 전달받은 사용자 정보가 있으면 우선 사용 (ScreenModal 등에서)
const userName = externalUserName || authUserName;
const user =
externalUserId && externalUserId !== authUser?.userId
? {
userId: externalUserId,
userName: externalUserName || authUserName || "",
companyCode: externalCompanyCode || authUser?.companyCode || "",
isAdmin: authUser?.isAdmin || false,
}
: authUser;
const [localFormData, setLocalFormData] = useState<Record<string, any>>({}); const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({}); const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
@ -130,59 +149,55 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const handleEnterKey = (e: KeyboardEvent) => { const handleEnterKey = (e: KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
// 한글 조합 중이면 무시 (한글 입력 문제 방지) // 한글 조합 중이면 무시 (한글 입력 문제 방지)
if ((e as any).isComposing || e.keyCode === 229) { if ((e as any).isComposing || e.keyCode === 229) {
return; return;
} }
// textarea는 제외 (여러 줄 입력) // textarea는 제외 (여러 줄 입력)
if (target.tagName === "TEXTAREA") { if (target.tagName === "TEXTAREA") {
return; return;
} }
// input, select 등의 폼 요소에서만 작동 // input, select 등의 폼 요소에서만 작동
if ( if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.getAttribute("role") === "combobox") {
target.tagName === "INPUT" ||
target.tagName === "SELECT" ||
target.getAttribute("role") === "combobox"
) {
e.preventDefault(); e.preventDefault();
// 모든 포커스 가능한 요소 찾기 // 모든 포커스 가능한 요소 찾기
const focusableElements = document.querySelectorAll<HTMLElement>( const focusableElements = document.querySelectorAll<HTMLElement>(
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])' 'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])',
); );
// 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬 // 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬
const focusableArray = Array.from(focusableElements).sort((a, b) => { const focusableArray = Array.from(focusableElements).sort((a, b) => {
const rectA = a.getBoundingClientRect(); const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect(); const rectB = b.getBoundingClientRect();
// Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로) // Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로)
if (Math.abs(rectA.top - rectB.top) > 10) { if (Math.abs(rectA.top - rectB.top) > 10) {
return rectA.top - rectB.top; return rectA.top - rectB.top;
} }
// 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로) // 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로)
return rectA.left - rectB.left; return rectA.left - rectB.left;
}); });
const currentIndex = focusableArray.indexOf(target); const currentIndex = focusableArray.indexOf(target);
if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) { if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) {
// 다음 요소로 포커스 이동 // 다음 요소로 포커스 이동
const nextElement = focusableArray[currentIndex + 1]; const nextElement = focusableArray[currentIndex + 1];
nextElement.focus(); nextElement.focus();
// select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지 // select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지
} }
} }
} }
}; };
document.addEventListener("keydown", handleEnterKey); document.addEventListener("keydown", handleEnterKey);
return () => { return () => {
document.removeEventListener("keydown", handleEnterKey); document.removeEventListener("keydown", handleEnterKey);
}; };
@ -193,31 +208,26 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const initAutoInputFields = async () => { const initAutoInputFields = async () => {
for (const comp of allComponents) { for (const comp of allComponents) {
// type: "component" 또는 type: "widget" 모두 처리 // type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') { if (comp.type === "widget" || comp.type === "component") {
const widget = comp as any; const widget = comp as any;
const fieldName = widget.columnName || widget.id; const fieldName = widget.columnName || widget.id;
// autoFill 처리 (테이블 조회 기반 자동 입력) // autoFill 처리 (테이블 조회 기반 자동 입력)
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
const autoFillConfig = widget.autoFill || (comp as any).autoFill; const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName]; const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') { if (currentValue === undefined || currentValue === "") {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기 // 사용자 정보에서 필터 값 가져오기
const userValue = user?.[userField]; const userValue = user?.[userField];
if (userValue && sourceTable && filterColumn && displayColumn) { if (userValue && sourceTable && filterColumn && displayColumn) {
try { try {
const { tableTypeApi } = await import("@/lib/api/screen"); const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord( const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn);
sourceTable,
filterColumn,
userValue,
displayColumn
);
updateFormData(fieldName, result.value); updateFormData(fieldName, result.value);
} catch (error) { } catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error); console.error(`autoFill 조회 실패: ${fieldName}`, error);
@ -329,10 +339,13 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
setFlowSelectedData(selectedData); setFlowSelectedData(selectedData);
setFlowSelectedStepId(stepId); setFlowSelectedStepId(stepId);
}} }}
onRefresh={onRefresh || (() => { onRefresh={
// 부모로부터 전달받은 onRefresh 또는 기본 동작 onRefresh ||
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출"); (() => {
})} // 부모로부터 전달받은 onRefresh 또는 기본 동작
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출");
})
}
onFlowRefresh={onFlowRefresh} onFlowRefresh={onFlowRefresh}
onClose={() => { onClose={() => {
// buttonActions.ts가 이미 처리함 // buttonActions.ts가 이미 처리함
@ -357,7 +370,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
return React.cloneElement(element, { return React.cloneElement(element, {
style: { style: {
...element.props.style, ...element.props.style,
...styleWithoutSize, // width/height 제외한 스타일만 적용 ...styleWithoutSize, // width/height 제외한 스타일만 적용
width: "100%", width: "100%",
height: "100%", height: "100%",
minHeight: "100%", minHeight: "100%",
@ -563,8 +576,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor, backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
color: config?.textColor || comp.style?.color, color: config?.textColor || comp.style?.color,
// 부모 컨테이너 크기에 맞춤 // 부모 컨테이너 크기에 맞춤
width: '100%', width: "100%",
height: '100%', height: "100%",
}} }}
> >
{label || "버튼"} {label || "버튼"}
@ -689,18 +702,18 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
// ✅ 격자 시스템 잔재 제거: style.width, style.height 무시 // ✅ 격자 시스템 잔재 제거: style.width, style.height 무시
const { width: styleWidth, height: styleHeight, ...styleWithoutSize } = style; const { width: styleWidth, height: styleHeight, ...styleWithoutSize } = style;
// TableSearchWidget의 경우 높이를 자동으로 설정 // TableSearchWidget의 경우 높이를 자동으로 설정
const isTableSearchWidget = (component as any).componentId === "table-search-widget"; const isTableSearchWidget = (component as any).componentId === "table-search-widget";
const componentStyle = { const componentStyle = {
position: "absolute" as const, position: "absolute" as const,
left: position?.x || 0, left: position?.x || 0,
top: position?.y || 0, top: position?.y || 0,
zIndex: position?.z || 1, zIndex: position?.z || 1,
...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용 ...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용
width: size?.width || 200, // size의 픽셀 값이 최종 우선순위 width: size?.width || 200, // size의 픽셀 값이 최종 우선순위
height: isTableSearchWidget ? "auto" : (size?.height || 10), height: isTableSearchWidget ? "auto" : size?.height || 10,
minHeight: isTableSearchWidget ? "48px" : undefined, minHeight: isTableSearchWidget ? "48px" : undefined,
}; };

View File

@ -125,7 +125,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
screenName: "", screenName: "",
description: "", description: "",
isActive: "Y", isActive: "Y",
tableName: "",
}); });
const [tables, setTables] = useState<string[]>([]);
const [loadingTables, setLoadingTables] = useState(false);
// 미리보기 관련 상태 // 미리보기 관련 상태
const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
@ -260,14 +263,31 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
onScreenSelect(screen); onScreenSelect(screen);
}; };
const handleEdit = (screen: ScreenDefinition) => { const handleEdit = async (screen: ScreenDefinition) => {
setScreenToEdit(screen); setScreenToEdit(screen);
setEditFormData({ setEditFormData({
screenName: screen.screenName, screenName: screen.screenName,
description: screen.description || "", description: screen.description || "",
isActive: screen.isActive, isActive: screen.isActive,
tableName: screen.tableName || "",
}); });
setEditDialogOpen(true); setEditDialogOpen(true);
// 테이블 목록 로드
try {
setLoadingTables(true);
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
// tableName만 추출 (camelCase)
const tableNames = response.data.map((table: any) => table.tableName);
setTables(tableNames);
}
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
} finally {
setLoadingTables(false);
}
}; };
const handleEditSave = async () => { const handleEditSave = async () => {
@ -1180,6 +1200,25 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
placeholder="화면명을 입력하세요" placeholder="화면명을 입력하세요"
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="edit-tableName"> *</Label>
<Select
value={editFormData.tableName}
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })}
disabled={loadingTables}
>
<SelectTrigger id="edit-tableName">
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
</SelectTrigger>
<SelectContent>
{tables.map((tableName) => (
<SelectItem key={tableName} value={tableName}>
{tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-description"></Label> <Label htmlFor="edit-description"></Label>
<Textarea <Textarea
@ -1210,7 +1249,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
<Button variant="outline" onClick={() => setEditDialogOpen(false)}> <Button variant="outline" onClick={() => setEditDialogOpen(false)}>
</Button> </Button>
<Button onClick={handleEditSave} disabled={!editFormData.screenName.trim()}> <Button onClick={handleEditSave} disabled={!editFormData.screenName.trim() || !editFormData.tableName.trim()}>
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { screenApi } from "@/lib/api/screen"; import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen"; import { ComponentData } from "@/types/screen";
import { useAuth } from "@/hooks/useAuth";
/** /**
* *
@ -24,6 +25,7 @@ export function ConditionalSectionViewer({
formData, formData,
onFormDataChange, onFormDataChange,
}: ConditionalSectionViewerProps) { }: ConditionalSectionViewerProps) {
const { userId, userName, user } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [components, setComponents] = useState<ComponentData[]>([]); const [components, setComponents] = useState<ComponentData[]>([]);
const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null); const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
@ -142,6 +144,9 @@ export function ConditionalSectionViewer({
onClick={() => {}} onClick={() => {}}
screenId={screenInfo?.id} screenId={screenInfo?.id}
tableName={screenInfo?.tableName} tableName={screenInfo?.tableName}
userId={userId}
userName={userName}
companyCode={user?.companyCode}
formData={formData} formData={formData}
onFormDataChange={onFormDataChange} onFormDataChange={onFormDataChange}
/> />