This commit is contained in:
parent
9597494685
commit
034ef59ef9
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue