Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feature/v2-unified-renewal
This commit is contained in:
@@ -108,7 +108,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
// 전체 카테고리 옵션 로드 (모든 테이블의 category 타입 컬럼)
|
||||
const loadAllCategoryOptions = async () => {
|
||||
try {
|
||||
// category_values_test 테이블에서 고유한 테이블.컬럼 조합 조회
|
||||
// category_values 테이블에서 고유한 테이블.컬럼 조합 조회
|
||||
const response = await getAllCategoryKeys();
|
||||
if (response.success && response.data) {
|
||||
const options: CategoryOption[] = response.data.map((item) => ({
|
||||
@@ -341,7 +341,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
ruleToSave,
|
||||
});
|
||||
|
||||
// 테스트 테이블에 저장 (numbering_rules_test)
|
||||
// 테스트 테이블에 저장 (numbering_rules)
|
||||
const response = await saveNumberingRuleToTest(ruleToSave);
|
||||
|
||||
if (response.success && response.data) {
|
||||
|
||||
@@ -253,6 +253,24 @@ export default function CopyScreenModal({
|
||||
}
|
||||
}, [useBulkRename, removeText, addPrefix]);
|
||||
|
||||
// 원본 회사가 선택된 경우 다른 회사로 자동 변경
|
||||
useEffect(() => {
|
||||
if (!companies.length || !isOpen) return;
|
||||
|
||||
const sourceCompanyCode = mode === "group"
|
||||
? sourceGroup?.company_code
|
||||
: sourceScreen?.companyCode;
|
||||
|
||||
// 원본 회사와 같은 회사가 선택되어 있으면 다른 회사로 변경
|
||||
if (sourceCompanyCode && targetCompanyCode === sourceCompanyCode) {
|
||||
const otherCompany = companies.find(c => c.companyCode !== sourceCompanyCode);
|
||||
if (otherCompany) {
|
||||
console.log("🔄 원본 회사 선택됨 → 다른 회사로 자동 변경:", otherCompany.companyCode);
|
||||
setTargetCompanyCode(otherCompany.companyCode);
|
||||
}
|
||||
}
|
||||
}, [companies, isOpen, mode, sourceGroup, sourceScreen, targetCompanyCode]);
|
||||
|
||||
// 대상 회사 변경 시 기존 코드 초기화
|
||||
useEffect(() => {
|
||||
if (targetCompanyCode) {
|
||||
@@ -1182,31 +1200,36 @@ export default function CopyScreenModal({
|
||||
// 그룹 복제 모드 렌더링
|
||||
if (mode === "group") {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
{/* 로딩 오버레이 */}
|
||||
{isCopying && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center rounded-lg bg-background/90 backdrop-blur-sm">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-primary" />
|
||||
<p className="mt-4 text-sm font-medium">{copyProgress.message}</p>
|
||||
<>
|
||||
{/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */}
|
||||
{isCopying && (
|
||||
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background/95 backdrop-blur-md">
|
||||
<div className="rounded-lg bg-card p-8 shadow-lg border flex flex-col items-center">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="mt-4 text-base font-medium">{copyProgress.message}</p>
|
||||
{copyProgress.total > 0 && (
|
||||
<>
|
||||
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-secondary">
|
||||
<div className="mt-4 h-3 w-64 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300"
|
||||
style={{ width: `${Math.round((copyProgress.current / copyProgress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{copyProgress.current} / {copyProgress.total} 화면
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
{copyProgress.current} / {copyProgress.total} 화면 복제 중...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p className="mt-4 text-xs text-muted-foreground">
|
||||
복제가 완료될 때까지 잠시 기다려주세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<FolderTree className="h-5 w-5" />
|
||||
그룹 복제
|
||||
</DialogTitle>
|
||||
@@ -1486,15 +1509,22 @@ export default function CopyScreenModal({
|
||||
onChange={(e) => setTargetCompanyCode(e.target.value)}
|
||||
className="mt-1 flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||
>
|
||||
{companies.map((company) => (
|
||||
<option key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName} ({company.companyCode})
|
||||
</option>
|
||||
))}
|
||||
{companies
|
||||
.filter((company) => company.companyCode !== sourceGroup?.company_code)
|
||||
.map((company) => (
|
||||
<option key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName} ({company.companyCode})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
|
||||
복제된 그룹과 화면이 이 회사에 생성됩니다
|
||||
</p>
|
||||
{sourceGroup && (
|
||||
<p className="mt-1 text-[10px] text-amber-600 sm:text-xs">
|
||||
* 원본 회사({sourceGroup.company_code})로는 복제할 수 없습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1590,14 +1620,25 @@ export default function CopyScreenModal({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 화면 복제 모드 렌더링
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<>
|
||||
{/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */}
|
||||
{isCopying && (
|
||||
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background/95 backdrop-blur-md">
|
||||
<div className="rounded-lg bg-card p-8 shadow-lg border flex flex-col items-center">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="mt-4 text-base font-medium">복제가 완료될 때까지 잠시 기다려주세요</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">화면 복제</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
"{sourceScreen?.screenName}" 화면을 복제합니다.
|
||||
@@ -1694,13 +1735,20 @@ export default function CopyScreenModal({
|
||||
<SelectValue placeholder="회사 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.map((company) => (
|
||||
<SelectItem key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
{companies
|
||||
.filter((company) => company.companyCode !== sourceScreen?.companyCode)
|
||||
.map((company) => (
|
||||
<SelectItem key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{sourceScreen && (
|
||||
<p className="mt-1 text-[10px] text-amber-600">
|
||||
* 원본 회사({sourceScreen.companyCode})로는 복제할 수 없습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1840,6 +1888,7 @@ export default function CopyScreenModal({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Building2,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import {
|
||||
@@ -1463,16 +1464,26 @@ export function ScreenGroupTreeView({
|
||||
|
||||
{/* 그룹 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px] border-destructive/50">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">그룹 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingGroup?.group_name}" 그룹을 삭제하시겠습니까?
|
||||
<br />
|
||||
{deleteScreensWithGroup
|
||||
? <span className="text-destructive font-medium">그룹에 속한 화면들도 함께 삭제됩니다.</span>
|
||||
: "그룹에 속한 화면들은 미분류로 이동됩니다."
|
||||
}
|
||||
<AlertDialogTitle className="text-base sm:text-lg flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
그룹 삭제 경고
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<div className="mt-2 rounded-md bg-destructive/10 border border-destructive/30 p-3">
|
||||
<p className="font-semibold text-destructive">
|
||||
"{deletingGroup?.group_name}" 그룹을 정말 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="mt-2 text-destructive/80">
|
||||
{deleteScreensWithGroup
|
||||
? "⚠️ 그룹에 속한 모든 화면, 플로우, 관련 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다."
|
||||
: "그룹에 속한 화면들은 미분류로 이동됩니다."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -1570,11 +1581,21 @@ export function ScreenGroupTreeView({
|
||||
)}
|
||||
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">화면 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingScreen?.screenName}" 화면을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 화면은 휴지통으로 이동됩니다.
|
||||
<AlertDialogTitle className="text-base sm:text-lg flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
화면 삭제 경고
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<div className="mt-2 rounded-md bg-destructive/10 border border-destructive/30 p-3">
|
||||
<p className="font-semibold text-destructive">
|
||||
"{deletingScreen?.screenName}" 화면을 정말 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="mt-2 text-destructive/80">
|
||||
⚠️ 화면과 연결된 플로우, 레이아웃 데이터가 모두 삭제됩니다. 삭제된 화면은 휴지통으로 이동됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||
|
||||
@@ -174,30 +174,10 @@ export default function TableTypeSelector({
|
||||
}
|
||||
};
|
||||
|
||||
// 입력 타입 변경
|
||||
const handleInputTypeChange = async (columnName: string, inputType: "direct" | "auto") => {
|
||||
try {
|
||||
// 현재 컬럼 정보 가져오기
|
||||
const currentColumn = columns.find((col) => col.columnName === columnName);
|
||||
if (!currentColumn) return;
|
||||
|
||||
// 웹 타입과 함께 입력 타입 업데이트
|
||||
await tableTypeApi.setColumnWebType(
|
||||
selectedTable,
|
||||
columnName,
|
||||
currentColumn.webType || "text",
|
||||
undefined, // detailSettings
|
||||
inputType,
|
||||
);
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
|
||||
|
||||
// console.log(`컬럼 ${columnName}의 입력 타입을 ${inputType}로 변경했습니다.`);
|
||||
} catch (error) {
|
||||
// console.error("입력 타입 변경 실패:", error);
|
||||
alert("입력 타입 설정에 실패했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
// 입력 타입 변경 (로컬 상태만 - DB에 저장하지 않음)
|
||||
const handleInputTypeChange = (columnName: string, inputType: "direct" | "auto") => {
|
||||
// 로컬 상태만 업데이트 (DB에는 저장하지 않음 - inputType은 화면 렌더링용)
|
||||
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
|
||||
};
|
||||
|
||||
const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
@@ -492,7 +492,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>((pro
|
||||
const categoryTable = (config as any).categoryTable;
|
||||
const categoryColumn = (config as any).categoryColumn;
|
||||
|
||||
// category 소스 유지 (category_values_test 테이블에서 로드)
|
||||
// category 소스 유지 (category_values 테이블에서 로드)
|
||||
const source = rawSource;
|
||||
const codeGroup = config.codeGroup;
|
||||
|
||||
@@ -612,7 +612,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>((pro
|
||||
fetchedOptions = data;
|
||||
}
|
||||
} else if (source === "category") {
|
||||
// 카테고리에서 로드 (category_values_test 테이블)
|
||||
// 카테고리에서 로드 (category_values 테이블)
|
||||
// tableName, columnName은 props에서 가져옴
|
||||
const catTable = categoryTable || tableName;
|
||||
const catColumn = categoryColumn || columnName;
|
||||
|
||||
@@ -470,7 +470,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||
const categoryTable = (config as any).categoryTable;
|
||||
const categoryColumn = (config as any).categoryColumn;
|
||||
|
||||
// category 소스 유지 (category_values_test 테이블에서 로드)
|
||||
// category 소스 유지 (category_values 테이블에서 로드)
|
||||
const source = rawSource;
|
||||
const codeGroup = config.codeGroup;
|
||||
|
||||
@@ -590,7 +590,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||
fetchedOptions = data;
|
||||
}
|
||||
} else if (source === "category") {
|
||||
// 카테고리에서 로드 (category_values_test 테이블)
|
||||
// 카테고리에서 로드 (category_values 테이블)
|
||||
// tableName, columnName은 props에서 가져옴
|
||||
const catTable = categoryTable || tableName;
|
||||
const catColumn = categoryColumn || columnName;
|
||||
|
||||
@@ -173,11 +173,11 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 테스트용 API (numbering_rules_test 테이블 사용) ======
|
||||
// ====== 테스트용 API (numbering_rules 테이블 사용) ======
|
||||
|
||||
/**
|
||||
* [테스트] 테스트 테이블에서 채번규칙 목록 조회
|
||||
* numbering_rules_test 테이블 사용
|
||||
* numbering_rules 테이블 사용
|
||||
* @param menuObjid 메뉴 OBJID (선택) - 필터링용
|
||||
*/
|
||||
export async function getNumberingRulesFromTest(
|
||||
@@ -199,7 +199,7 @@ export async function getNumberingRulesFromTest(
|
||||
|
||||
/**
|
||||
* [테스트] 테이블+컬럼 기반 채번규칙 조회
|
||||
* numbering_rules_test 테이블 사용
|
||||
* numbering_rules 테이블 사용
|
||||
*/
|
||||
export async function getNumberingRuleByColumn(
|
||||
tableName: string,
|
||||
@@ -220,7 +220,7 @@ export async function getNumberingRuleByColumn(
|
||||
|
||||
/**
|
||||
* [테스트] 테스트 테이블에 채번규칙 저장
|
||||
* numbering_rules_test 테이블 사용
|
||||
* numbering_rules 테이블 사용
|
||||
*/
|
||||
export async function saveNumberingRuleToTest(
|
||||
config: NumberingRuleConfig
|
||||
@@ -238,7 +238,7 @@ export async function saveNumberingRuleToTest(
|
||||
|
||||
/**
|
||||
* [테스트] 테스트 테이블에서 채번규칙 삭제
|
||||
* numbering_rules_test 테이블 사용
|
||||
* numbering_rules 테이블 사용
|
||||
*/
|
||||
export async function deleteNumberingRuleFromTest(
|
||||
ruleId: string
|
||||
|
||||
@@ -347,12 +347,10 @@ export const tableTypeApi = {
|
||||
columnName: string,
|
||||
webType: string,
|
||||
detailSettings?: Record<string, any>,
|
||||
inputType?: "direct" | "auto",
|
||||
): Promise<void> => {
|
||||
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
|
||||
webType,
|
||||
detailSettings,
|
||||
inputType,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export interface NumberingRuleConfig {
|
||||
|
||||
// 카테고리 조건 (특정 카테고리 값일 때만 이 규칙 적용)
|
||||
categoryColumn?: string; // 카테고리 조건 컬럼명 (예: 'type', 'material')
|
||||
categoryValueId?: number; // 카테고리 값 ID (category_values_test.value_id)
|
||||
categoryValueId?: number; // 카테고리 값 ID (category_values.value_id)
|
||||
categoryValueLabel?: string; // 카테고리 값 라벨 (조회 시 조인)
|
||||
|
||||
// 메타 정보
|
||||
|
||||
Reference in New Issue
Block a user