From 034ef59ef93d730b8f903d70392891d69fa4bd56 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 19 Dec 2025 16:20:59 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/services/menuCopyService.ts | 65 +- .../screen/panels/UnifiedPropertiesPanel.tsx | 23 +- frontend/components/unified/UnifiedDate.tsx | 561 +++++++++--------- .../config-panels/UnifiedInputConfigPanel.tsx | 15 +- 4 files changed, 289 insertions(+), 375 deletions(-) diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index 87ba1d56..2dc70d0c 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -738,43 +738,28 @@ export class MenuCopyService { ]); logger.info(` ✅ 메뉴 권한 삭제 완료`); -<<<<<<< HEAD - // 5-4. 채번 규칙 처리 (외래키 제약조건 해결) - // scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함) -======= // 5-4. 채번 규칙 처리 (체크 제약조건 고려) // scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함) // check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수 ->>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176 const menuScopedRulesResult = await client.query( `SELECT rule_id FROM numbering_rules WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`, [existingMenuIds, targetCompanyCode] ); if (menuScopedRulesResult.rows.length > 0) { -<<<<<<< HEAD - const menuScopedRuleIds = menuScopedRulesResult.rows.map(r => r.rule_id); -======= const menuScopedRuleIds = menuScopedRulesResult.rows.map( (r) => r.rule_id ); ->>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176 // 채번 규칙 파트 먼저 삭제 await client.query( `DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`, [menuScopedRuleIds] ); // 채번 규칙 삭제 -<<<<<<< HEAD await client.query( `DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, [menuScopedRuleIds] ); - logger.info(` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개`); -======= - await client.query(`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, [ - menuScopedRuleIds, - ]); logger.info( ` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개` ); @@ -818,37 +803,7 @@ export class MenuCopyService { await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [ existingMenuIds, ]); ->>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176 } - - // scope_type != 'menu'인 채번 규칙: menu_objid만 NULL로 설정 (규칙 보존) - const tableScopedRulesResult = await client.query( - `UPDATE numbering_rules - SET menu_objid = NULL - WHERE menu_objid = ANY($1) AND company_code = $2 AND (scope_type IS NULL OR scope_type != 'menu') - RETURNING rule_id`, - [existingMenuIds, targetCompanyCode] - ); - if (tableScopedRulesResult.rows.length > 0) { - logger.info(` ✅ 테이블 스코프 채번 규칙 연결 해제: ${tableScopedRulesResult.rows.length}개 (데이터 보존)`); - } - - // 5-5. 카테고리 컬럼 매핑 삭제 (NOT NULL 제약조건으로 인해 삭제) - const deletedCategoryMappings = await client.query( - `DELETE FROM category_column_mapping - WHERE menu_objid = ANY($1) AND company_code = $2 - RETURNING mapping_id`, - [existingMenuIds, targetCompanyCode] - ); - if (deletedCategoryMappings.rows.length > 0) { - logger.info(` ✅ 카테고리 매핑 삭제: ${deletedCategoryMappings.rows.length}개`); - } - - // 5-6. 메뉴 삭제 (배치) - await client.query( - `DELETE FROM menu_info WHERE objid = ANY($1)`, - [existingMenuIds] - ); logger.info(` ✅ 메뉴 삭제 완료: ${existingMenus.length}개`); logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨"); @@ -2416,12 +2371,8 @@ export class MenuCopyService { return { copiedCount, ruleIdMap }; } -<<<<<<< HEAD - // 2. 대상 회사에 이미 존재하는 모든 채번 규칙 조회 (원본 ID + 새로 생성될 ID 모두 체크) -======= // 2. 대상 회사에 이미 존재하는 채번 규칙 한 번에 조회 const ruleIds = allRulesResult.rows.map((r) => r.rule_id); ->>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176 const existingRulesResult = await client.query( `SELECT rule_id FROM numbering_rules WHERE company_code = $1`, [targetCompanyCode] @@ -2438,22 +2389,9 @@ export class MenuCopyService { const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = []; for (const rule of allRulesResult.rows) { - // 새 rule_id 생성 - const originalSuffix = rule.rule_id.includes('_') - ? rule.rule_id.replace(/^[^_]*_/, '') - : rule.rule_id; - const newRuleId = `${targetCompanyCode}_${originalSuffix}`; - - // 원본 ID 또는 새로 생성될 ID가 이미 존재하는 경우 스킵 if (existingRuleIds.has(rule.rule_id)) { + // 기존 규칙은 동일한 ID로 매핑 ruleIdMap.set(rule.rule_id, rule.rule_id); -<<<<<<< HEAD - logger.info(` ♻️ 채번규칙 이미 존재 (원본 ID): ${rule.rule_id}`); - } else if (existingRuleIds.has(newRuleId)) { - ruleIdMap.set(rule.rule_id, newRuleId); - logger.info(` ♻️ 채번규칙 이미 존재 (대상 ID): ${newRuleId}`); - } else { -======= // 새 메뉴 ID로 연결 업데이트 필요 const newMenuObjid = menuIdMap.get(rule.menu_objid); @@ -2470,7 +2408,6 @@ export class MenuCopyService { : rule.rule_id; const newRuleId = `${targetCompanyCode}_${originalSuffix}`; ->>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176 ruleIdMap.set(rule.rule_id, newRuleId); originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId }); rulesToCopy.push({ ...rule, newRuleId }); diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index b8ec467a..83dde425 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -319,14 +319,17 @@ export const UnifiedPropertiesPanel: React.FC = ({ if (componentId?.startsWith("unified-")) { const unifiedConfigPanels: Record void }>> = { "unified-input": require("@/components/unified/config-panels/UnifiedInputConfigPanel").UnifiedInputConfigPanel, - "unified-select": require("@/components/unified/config-panels/UnifiedSelectConfigPanel").UnifiedSelectConfigPanel, + "unified-select": require("@/components/unified/config-panels/UnifiedSelectConfigPanel") + .UnifiedSelectConfigPanel, "unified-date": require("@/components/unified/config-panels/UnifiedDateConfigPanel").UnifiedDateConfigPanel, "unified-list": require("@/components/unified/config-panels/UnifiedListConfigPanel").UnifiedListConfigPanel, - "unified-layout": require("@/components/unified/config-panels/UnifiedLayoutConfigPanel").UnifiedLayoutConfigPanel, + "unified-layout": require("@/components/unified/config-panels/UnifiedLayoutConfigPanel") + .UnifiedLayoutConfigPanel, "unified-group": require("@/components/unified/config-panels/UnifiedGroupConfigPanel").UnifiedGroupConfigPanel, "unified-media": require("@/components/unified/config-panels/UnifiedMediaConfigPanel").UnifiedMediaConfigPanel, "unified-biz": require("@/components/unified/config-panels/UnifiedBizConfigPanel").UnifiedBizConfigPanel, - "unified-hierarchy": require("@/components/unified/config-panels/UnifiedHierarchyConfigPanel").UnifiedHierarchyConfigPanel, + "unified-hierarchy": require("@/components/unified/config-panels/UnifiedHierarchyConfigPanel") + .UnifiedHierarchyConfigPanel, }; const UnifiedConfigPanel = unifiedConfigPanels[componentId]; @@ -1038,9 +1041,7 @@ export const UnifiedPropertiesPanel: React.FC = ({ // 🆕 3.5. Unified 컴포넌트 - 반드시 다른 체크보다 먼저 처리 const unifiedComponentType = - (selectedComponent as any).componentType || - selectedComponent.componentConfig?.type || - ""; + (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || ""; if (unifiedComponentType.startsWith("unified-")) { const configPanel = renderComponentConfigPanel(); if (configPanel) { @@ -1538,7 +1539,15 @@ export const UnifiedPropertiesPanel: React.FC = ({
{ handleUpdate("conditional", newConfig); }} diff --git a/frontend/components/unified/UnifiedDate.tsx b/frontend/components/unified/UnifiedDate.tsx index da5d0cc0..5f63d701 100644 --- a/frontend/components/unified/UnifiedDate.tsx +++ b/frontend/components/unified/UnifiedDate.tsx @@ -2,7 +2,7 @@ /** * UnifiedDate - * + * * 통합 날짜/시간 컴포넌트 * - date: 날짜 선택 * - time: 시간 선택 @@ -37,14 +37,14 @@ const DATE_FORMATS: Record = { // 날짜 문자열 → Date 객체 function parseDate(value: string | undefined, formatStr: string): Date | undefined { if (!value) return undefined; - + const dateFnsFormat = DATE_FORMATS[formatStr] || formatStr; - + try { // ISO 형식 먼저 시도 const isoDate = new Date(value); if (isValid(isoDate)) return isoDate; - + // 포맷에 맞게 파싱 const parsed = parse(value, dateFnsFormat, new Date()); return isValid(parsed) ? parsed : undefined; @@ -63,151 +63,152 @@ function formatDate(date: Date | undefined, formatStr: string): string { /** * 단일 날짜 선택 컴포넌트 */ -const SingleDatePicker = forwardRef void; - dateFormat: string; - showToday?: boolean; - minDate?: string; - maxDate?: string; - disabled?: boolean; - readonly?: boolean; - className?: string; -}>(({ - value, - onChange, - dateFormat = "YYYY-MM-DD", - showToday = true, - minDate, - maxDate, - disabled, - readonly, - className -}, ref) => { - const [open, setOpen] = useState(false); - - const date = useMemo(() => parseDate(value, dateFormat), [value, dateFormat]); - const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]); - const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]); +const SingleDatePicker = forwardRef< + HTMLButtonElement, + { + value?: string; + onChange?: (value: string) => void; + dateFormat: string; + showToday?: boolean; + minDate?: string; + maxDate?: string; + disabled?: boolean; + readonly?: boolean; + className?: string; + } +>( + ( + { value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className }, + ref, + ) => { + const [open, setOpen] = useState(false); - const handleSelect = useCallback((selectedDate: Date | undefined) => { - if (selectedDate) { - onChange?.(formatDate(selectedDate, dateFormat)); + const date = useMemo(() => parseDate(value, dateFormat), [value, dateFormat]); + const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]); + const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]); + + const handleSelect = useCallback( + (selectedDate: Date | undefined) => { + if (selectedDate) { + onChange?.(formatDate(selectedDate, dateFormat)); + setOpen(false); + } + }, + [dateFormat, onChange], + ); + + const handleToday = useCallback(() => { + onChange?.(formatDate(new Date(), dateFormat)); setOpen(false); - } - }, [dateFormat, onChange]); + }, [dateFormat, onChange]); - const handleToday = useCallback(() => { - onChange?.(formatDate(new Date(), dateFormat)); - setOpen(false); - }, [dateFormat, onChange]); + const handleClear = useCallback(() => { + onChange?.(""); + setOpen(false); + }, [onChange]); - const handleClear = useCallback(() => { - onChange?.(""); - setOpen(false); - }, [onChange]); - - return ( - - - - - - { - if (minDateObj && date < minDateObj) return true; - if (maxDateObj && date > maxDateObj) return true; - return false; - }} - /> -
- {showToday && ( - - )} - -
-
-
- ); -}); + + + { + if (minDateObj && date < minDateObj) return true; + if (maxDateObj && date > maxDateObj) return true; + return false; + }} + /> +
+ {showToday && ( + + )} + +
+
+ + ); + }, +); SingleDatePicker.displayName = "SingleDatePicker"; /** * 날짜 범위 선택 컴포넌트 */ -const RangeDatePicker = forwardRef void; - dateFormat: string; - minDate?: string; - maxDate?: string; - disabled?: boolean; - readonly?: boolean; - className?: string; -}>(({ - value = ["", ""], - onChange, - dateFormat = "YYYY-MM-DD", - minDate, - maxDate, - disabled, - readonly, - className -}, ref) => { +const RangeDatePicker = forwardRef< + HTMLDivElement, + { + value?: [string, string]; + onChange?: (value: [string, string]) => void; + dateFormat: string; + minDate?: string; + maxDate?: string; + disabled?: boolean; + readonly?: boolean; + className?: string; + } +>(({ value = ["", ""], onChange, dateFormat = "YYYY-MM-DD", minDate, maxDate, disabled, readonly, className }, ref) => { const [openStart, setOpenStart] = useState(false); const [openEnd, setOpenEnd] = useState(false); - + const startDate = useMemo(() => parseDate(value[0], dateFormat), [value, dateFormat]); const endDate = useMemo(() => parseDate(value[1], dateFormat), [value, dateFormat]); const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]); const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]); - const handleStartSelect = useCallback((date: Date | undefined) => { - if (date) { - const newStart = formatDate(date, dateFormat); - // 시작일이 종료일보다 크면 종료일도 같이 변경 - if (endDate && date > endDate) { - onChange?.([newStart, newStart]); - } else { - onChange?.([newStart, value[1]]); + const handleStartSelect = useCallback( + (date: Date | undefined) => { + if (date) { + const newStart = formatDate(date, dateFormat); + // 시작일이 종료일보다 크면 종료일도 같이 변경 + if (endDate && date > endDate) { + onChange?.([newStart, newStart]); + } else { + onChange?.([newStart, value[1]]); + } + setOpenStart(false); } - setOpenStart(false); - } - }, [value, dateFormat, endDate, onChange]); + }, + [value, dateFormat, endDate, onChange], + ); - const handleEndSelect = useCallback((date: Date | undefined) => { - if (date) { - const newEnd = formatDate(date, dateFormat); - // 종료일이 시작일보다 작으면 시작일도 같이 변경 - if (startDate && date < startDate) { - onChange?.([newEnd, newEnd]); - } else { - onChange?.([value[0], newEnd]); + const handleEndSelect = useCallback( + (date: Date | undefined) => { + if (date) { + const newEnd = formatDate(date, dateFormat); + // 종료일이 시작일보다 작으면 시작일도 같이 변경 + if (startDate && date < startDate) { + onChange?.([newEnd, newEnd]); + } else { + onChange?.([value[0], newEnd]); + } + setOpenEnd(false); } - setOpenEnd(false); - } - }, [value, dateFormat, startDate, onChange]); + }, + [value, dateFormat, startDate, onChange], + ); return (
@@ -217,10 +218,7 @@ const RangeDatePicker = forwardRef {value[0] || "시작일"} @@ -250,10 +248,7 @@ const RangeDatePicker = forwardRef {value[1] || "종료일"} @@ -284,16 +279,19 @@ RangeDatePicker.displayName = "RangeDatePicker"; /** * 시간 선택 컴포넌트 */ -const TimePicker = forwardRef void; - disabled?: boolean; - readonly?: boolean; - className?: string; -}>(({ value, onChange, disabled, readonly, className }, ref) => { +const TimePicker = forwardRef< + HTMLInputElement, + { + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; + readonly?: boolean; + className?: string; + } +>(({ value, onChange, disabled, readonly, className }, ref) => { return (
- + void; - dateFormat: string; - minDate?: string; - maxDate?: string; - disabled?: boolean; - readonly?: boolean; - className?: string; -}>(({ - value, - onChange, - dateFormat = "YYYY-MM-DD HH:mm", - minDate, - maxDate, - disabled, - readonly, - className -}, ref) => { +const DateTimePicker = forwardRef< + HTMLDivElement, + { + value?: string; + onChange?: (value: string) => void; + dateFormat: string; + minDate?: string; + maxDate?: string; + disabled?: boolean; + readonly?: boolean; + className?: string; + } +>(({ value, onChange, dateFormat = "YYYY-MM-DD HH:mm", minDate, maxDate, disabled, readonly, className }, ref) => { // 날짜와 시간 분리 const [datePart, timePart] = useMemo(() => { if (!value) return ["", ""]; @@ -337,15 +329,21 @@ const DateTimePicker = forwardRef { - const newValue = `${newDate} ${timePart || "00:00"}`; - onChange?.(newValue.trim()); - }, [timePart, onChange]); + const handleDateChange = useCallback( + (newDate: string) => { + const newValue = `${newDate} ${timePart || "00:00"}`; + onChange?.(newValue.trim()); + }, + [timePart, onChange], + ); - const handleTimeChange = useCallback((newTime: string) => { - const newValue = `${datePart || format(new Date(), "yyyy-MM-dd")} ${newTime}`; - onChange?.(newValue.trim()); - }, [datePart, onChange]); + const handleTimeChange = useCallback( + (newTime: string) => { + const newValue = `${datePart || format(new Date(), "yyyy-MM-dd")} ${newTime}`; + onChange?.(newValue.trim()); + }, + [datePart, onChange], + ); return (
@@ -361,12 +359,7 @@ const DateTimePicker = forwardRef
- +
); @@ -376,36 +369,64 @@ DateTimePicker.displayName = "DateTimePicker"; /** * 메인 UnifiedDate 컴포넌트 */ -export const UnifiedDate = forwardRef( - (props, ref) => { - const { - id, - label, - required, - readonly, - disabled, - style, - size, - config: configProp, - value, - onChange, - } = props; +export const UnifiedDate = forwardRef((props, ref) => { + const { id, label, required, readonly, disabled, style, size, config: configProp, value, onChange } = props; - // config가 없으면 기본값 사용 - const config = configProp || { type: "date" as const }; + // config가 없으면 기본값 사용 + const config = configProp || { type: "date" as const }; - const dateFormat = config.format || "YYYY-MM-DD"; + const dateFormat = config.format || "YYYY-MM-DD"; - // 타입별 컴포넌트 렌더링 - const renderDatePicker = () => { - const isDisabled = disabled || readonly; + // 타입별 컴포넌트 렌더링 + const renderDatePicker = () => { + const isDisabled = disabled || readonly; - // 범위 선택 - if (config.range) { + // 범위 선택 + if (config.range) { + return ( + void} + dateFormat={dateFormat} + minDate={config.minDate} + maxDate={config.maxDate} + disabled={isDisabled} + readonly={readonly} + /> + ); + } + + // 타입별 렌더링 + switch (config.type) { + case "date": return ( - void} + onChange?.(v)} + dateFormat={dateFormat} + showToday={config.showToday} + minDate={config.minDate} + maxDate={config.maxDate} + disabled={isDisabled} + readonly={readonly} + /> + ); + + case "time": + return ( + onChange?.(v)} + disabled={isDisabled} + readonly={readonly} + /> + ); + + case "datetime": + return ( + onChange?.(v)} dateFormat={dateFormat} minDate={config.minDate} maxDate={config.maxDate} @@ -413,99 +434,55 @@ export const UnifiedDate = forwardRef( readonly={readonly} /> ); - } - // 타입별 렌더링 - switch (config.type) { - case "date": - return ( - onChange?.(v)} - dateFormat={dateFormat} - showToday={config.showToday} - minDate={config.minDate} - maxDate={config.maxDate} - disabled={isDisabled} - readonly={readonly} - /> - ); + default: + return ( + onChange?.(v)} + dateFormat={dateFormat} + showToday={config.showToday} + disabled={isDisabled} + readonly={readonly} + /> + ); + } + }; - case "time": - return ( - onChange?.(v)} - disabled={isDisabled} - readonly={readonly} - /> - ); + const showLabel = label && style?.labelDisplay !== false; + const componentWidth = size?.width || style?.width; + const componentHeight = size?.height || style?.height; - case "datetime": - return ( - onChange?.(v)} - dateFormat={dateFormat} - minDate={config.minDate} - maxDate={config.maxDate} - disabled={isDisabled} - readonly={readonly} - /> - ); - - default: - return ( - onChange?.(v)} - dateFormat={dateFormat} - showToday={config.showToday} - disabled={isDisabled} - readonly={readonly} - /> - ); - } - }; - - const showLabel = label && style?.labelDisplay !== false; - const componentWidth = size?.width || style?.width; - const componentHeight = size?.height || style?.height; - - return ( -
- {showLabel && ( - - )} -
- {renderDatePicker()} -
-
- ); - } -); + return ( +
+ {showLabel && ( + + )} +
{renderDatePicker()}
+
+ ); +}); UnifiedDate.displayName = "UnifiedDate"; export default UnifiedDate; - diff --git a/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx index eed7fe62..c0059a55 100644 --- a/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedInputConfigPanel.tsx @@ -16,10 +16,7 @@ interface UnifiedInputConfigPanelProps { onChange: (config: Record) => void; } -export const UnifiedInputConfigPanel: React.FC = ({ - config, - onChange, -}) => { +export const UnifiedInputConfigPanel: React.FC = ({ config, onChange }) => { // 설정 업데이트 핸들러 const updateConfig = (field: string, value: any) => { onChange({ ...config, [field]: value }); @@ -54,10 +51,7 @@ export const UnifiedInputConfigPanel: React.FC = ( {(config.inputType === "text" || !config.inputType) && (
- updateConfig("format", value)}> @@ -147,9 +141,7 @@ export const UnifiedInputConfigPanel: React.FC = ( placeholder="예: ###-####-####" className="h-8 text-xs" /> -

- # = 숫자, A = 문자, * = 모든 문자 -

+

# = 숫자, A = 문자, * = 모든 문자

); @@ -158,4 +150,3 @@ export const UnifiedInputConfigPanel: React.FC = ( UnifiedInputConfigPanel.displayName = "UnifiedInputConfigPanel"; export default UnifiedInputConfigPanel; -