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:
kjs
2026-02-03 09:34:33 +09:00
20 changed files with 534 additions and 479 deletions

View File

@@ -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) {

View File

@@ -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>
</>
);
}

View File

@@ -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">
&quot;{deletingGroup?.group_name}&quot; ?
</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">
&quot;{deletingScreen?.screenName}&quot; ?
</p>
<p className="mt-2 text-destructive/80">
, . .
</p>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2 sm:gap-0">

View File

@@ -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()));

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
});
},

View File

@@ -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; // 카테고리 값 라벨 (조회 시 조인)
// 메타 정보