This commit is contained in:
kjs 2025-12-19 16:20:59 +09:00
parent 9597494685
commit 034ef59ef9
4 changed files with 289 additions and 375 deletions

View File

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

View File

@ -319,14 +319,17 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
if (componentId?.startsWith("unified-")) {
const unifiedConfigPanels: Record<string, React.FC<{ config: any; onChange: (config: any) => 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<UnifiedPropertiesPanelProps> = ({
// 🆕 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<UnifiedPropertiesPanelProps> = ({
</div>
<div className="rounded-md border border-gray-200 p-2">
<ConditionalConfigPanel
config={(selectedComponent as any).conditional || { enabled: false, field: "", operator: "=", value: "", action: "show" }}
config={
(selectedComponent as any).conditional || {
enabled: false,
field: "",
operator: "=",
value: "",
action: "show",
}
}
onChange={(newConfig: ConditionalConfig) => {
handleUpdate("conditional", newConfig);
}}

View File

@ -2,7 +2,7 @@
/**
* UnifiedDate
*
*
* /
* - date: 날짜
* - time: 시간
@ -37,14 +37,14 @@ const DATE_FORMATS: Record<string, string> = {
// 날짜 문자열 → 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<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 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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
ref={ref}
variant="outline"
disabled={disabled || readonly}
className={cn(
"h-10 w-full justify-start text-left font-normal",
!value && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value || "날짜 선택"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
initialFocus
locale={ko}
disabled={(date) => {
if (minDateObj && date < minDateObj) return true;
if (maxDateObj && date > maxDateObj) return true;
return false;
}}
/>
<div className="flex gap-2 p-3 pt-0">
{showToday && (
<Button variant="outline" size="sm" onClick={handleToday}>
</Button>
)}
<Button variant="ghost" size="sm" onClick={handleClear}>
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
ref={ref}
variant="outline"
disabled={disabled || readonly}
className={cn(
"h-10 w-full justify-start text-left font-normal",
!value && "text-muted-foreground",
className,
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value || "날짜 선택"}
</Button>
</div>
</PopoverContent>
</Popover>
);
});
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
initialFocus
locale={ko}
disabled={(date) => {
if (minDateObj && date < minDateObj) return true;
if (maxDateObj && date > maxDateObj) return true;
return false;
}}
/>
<div className="flex gap-2 p-3 pt-0">
{showToday && (
<Button variant="outline" size="sm" onClick={handleToday}>
</Button>
)}
<Button variant="ghost" size="sm" onClick={handleClear}>
</Button>
</div>
</PopoverContent>
</Popover>
);
},
);
SingleDatePicker.displayName = "SingleDatePicker";
/**
*
*/
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 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 (
<div ref={ref} className={cn("flex items-center gap-2", className)}>
@ -217,10 +218,7 @@ const RangeDatePicker = forwardRef<HTMLDivElement, {
<Button
variant="outline"
disabled={disabled || readonly}
className={cn(
"h-10 flex-1 justify-start text-left font-normal",
!value[0] && "text-muted-foreground"
)}
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[0] && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value[0] || "시작일"}
@ -250,10 +248,7 @@ const RangeDatePicker = forwardRef<HTMLDivElement, {
<Button
variant="outline"
disabled={disabled || readonly}
className={cn(
"h-10 flex-1 justify-start text-left font-normal",
!value[1] && "text-muted-foreground"
)}
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[1] && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value[1] || "종료일"}
@ -284,16 +279,19 @@ RangeDatePicker.displayName = "RangeDatePicker";
/**
*
*/
const TimePicker = forwardRef<HTMLInputElement, {
value?: string;
onChange?: (value: string) => 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 (
<div className={cn("relative", className)}>
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Clock className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
ref={ref}
type="time"
@ -311,25 +309,19 @@ TimePicker.displayName = "TimePicker";
/**
* +
*/
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 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<HTMLDivElement, {
return [parts[0] || "", parts[1] || ""];
}, [value]);
const handleDateChange = useCallback((newDate: string) => {
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 (
<div ref={ref} className={cn("flex gap-2", className)}>
@ -361,12 +359,7 @@ const DateTimePicker = forwardRef<HTMLDivElement, {
/>
</div>
<div className="w-32">
<TimePicker
value={timePart}
onChange={handleTimeChange}
disabled={disabled}
readonly={readonly}
/>
<TimePicker value={timePart} onChange={handleTimeChange} disabled={disabled} readonly={readonly} />
</div>
</div>
);
@ -376,36 +369,64 @@ DateTimePicker.displayName = "DateTimePicker";
/**
* UnifiedDate
*/
export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>(
(props, ref) => {
const {
id,
label,
required,
readonly,
disabled,
style,
size,
config: configProp,
value,
onChange,
} = props;
export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>((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 (
<RangeDatePicker
value={Array.isArray(value) ? (value as [string, string]) : ["", ""]}
onChange={onChange as (value: [string, string]) => void}
dateFormat={dateFormat}
minDate={config.minDate}
maxDate={config.maxDate}
disabled={isDisabled}
readonly={readonly}
/>
);
}
// 타입별 렌더링
switch (config.type) {
case "date":
return (
<RangeDatePicker
value={Array.isArray(value) ? value as [string, string] : ["", ""]}
onChange={onChange as (value: [string, string]) => void}
<SingleDatePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
dateFormat={dateFormat}
showToday={config.showToday}
minDate={config.minDate}
maxDate={config.maxDate}
disabled={isDisabled}
readonly={readonly}
/>
);
case "time":
return (
<TimePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
disabled={isDisabled}
readonly={readonly}
/>
);
case "datetime":
return (
<DateTimePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
dateFormat={dateFormat}
minDate={config.minDate}
maxDate={config.maxDate}
@ -413,99 +434,55 @@ export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>(
readonly={readonly}
/>
);
}
// 타입별 렌더링
switch (config.type) {
case "date":
return (
<SingleDatePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
dateFormat={dateFormat}
showToday={config.showToday}
minDate={config.minDate}
maxDate={config.maxDate}
disabled={isDisabled}
readonly={readonly}
/>
);
default:
return (
<SingleDatePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
dateFormat={dateFormat}
showToday={config.showToday}
disabled={isDisabled}
readonly={readonly}
/>
);
}
};
case "time":
return (
<TimePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => 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 (
<DateTimePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => onChange?.(v)}
dateFormat={dateFormat}
minDate={config.minDate}
maxDate={config.maxDate}
disabled={isDisabled}
readonly={readonly}
/>
);
default:
return (
<SingleDatePicker
value={typeof value === "string" ? value : ""}
onChange={(v) => 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 (
<div
ref={ref}
id={id}
className="flex flex-col"
style={{
width: componentWidth,
height: componentHeight,
}}
>
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
}}
className="text-sm font-medium flex-shrink-0"
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
<div className="flex-1 min-h-0">
{renderDatePicker()}
</div>
</div>
);
}
);
return (
<div
ref={ref}
id={id}
className="flex flex-col"
style={{
width: componentWidth,
height: componentHeight,
}}
>
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
}}
className="flex-shrink-0 text-sm font-medium"
>
{label}
{required && <span className="ml-0.5 text-orange-500">*</span>}
</Label>
)}
<div className="min-h-0 flex-1">{renderDatePicker()}</div>
</div>
);
});
UnifiedDate.displayName = "UnifiedDate";
export default UnifiedDate;

View File

@ -16,10 +16,7 @@ interface UnifiedInputConfigPanelProps {
onChange: (config: Record<string, any>) => void;
}
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({
config,
onChange,
}) => {
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({ config, onChange }) => {
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value });
@ -54,10 +51,7 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
{(config.inputType === "text" || !config.inputType) && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select
value={config.format || "none"}
onValueChange={(value) => updateConfig("format", value)}
>
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="형식 선택" />
</SelectTrigger>
@ -147,9 +141,7 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
placeholder="예: ###-####-####"
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
# = , A = , * =
</p>
<p className="text-muted-foreground text-[10px]"># = , A = , * = </p>
</div>
</div>
);
@ -158,4 +150,3 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
UnifiedInputConfigPanel.displayName = "UnifiedInputConfigPanel";
export default UnifiedInputConfigPanel;