This commit is contained in:
parent
9597494685
commit
034ef59ef9
|
|
@ -738,43 +738,28 @@ export class MenuCopyService {
|
||||||
]);
|
]);
|
||||||
logger.info(` ✅ 메뉴 권한 삭제 완료`);
|
logger.info(` ✅ 메뉴 권한 삭제 완료`);
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
// 5-4. 채번 규칙 처리 (외래키 제약조건 해결)
|
|
||||||
// scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함)
|
|
||||||
=======
|
|
||||||
// 5-4. 채번 규칙 처리 (체크 제약조건 고려)
|
// 5-4. 채번 규칙 처리 (체크 제약조건 고려)
|
||||||
// scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함)
|
// scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함)
|
||||||
// check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수
|
// check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수
|
||||||
>>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176
|
|
||||||
const menuScopedRulesResult = await client.query(
|
const menuScopedRulesResult = await client.query(
|
||||||
`SELECT rule_id FROM numbering_rules
|
`SELECT rule_id FROM numbering_rules
|
||||||
WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`,
|
WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`,
|
||||||
[existingMenuIds, targetCompanyCode]
|
[existingMenuIds, targetCompanyCode]
|
||||||
);
|
);
|
||||||
if (menuScopedRulesResult.rows.length > 0) {
|
if (menuScopedRulesResult.rows.length > 0) {
|
||||||
<<<<<<< HEAD
|
|
||||||
const menuScopedRuleIds = menuScopedRulesResult.rows.map(r => r.rule_id);
|
|
||||||
=======
|
|
||||||
const menuScopedRuleIds = menuScopedRulesResult.rows.map(
|
const menuScopedRuleIds = menuScopedRulesResult.rows.map(
|
||||||
(r) => r.rule_id
|
(r) => r.rule_id
|
||||||
);
|
);
|
||||||
>>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176
|
|
||||||
// 채번 규칙 파트 먼저 삭제
|
// 채번 규칙 파트 먼저 삭제
|
||||||
await client.query(
|
await client.query(
|
||||||
`DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`,
|
`DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`,
|
||||||
[menuScopedRuleIds]
|
[menuScopedRuleIds]
|
||||||
);
|
);
|
||||||
// 채번 규칙 삭제
|
// 채번 규칙 삭제
|
||||||
<<<<<<< HEAD
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`,
|
`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`,
|
||||||
[menuScopedRuleIds]
|
[menuScopedRuleIds]
|
||||||
);
|
);
|
||||||
logger.info(` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개`);
|
|
||||||
=======
|
|
||||||
await client.query(`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, [
|
|
||||||
menuScopedRuleIds,
|
|
||||||
]);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개`
|
` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개`
|
||||||
);
|
);
|
||||||
|
|
@ -818,37 +803,7 @@ export class MenuCopyService {
|
||||||
await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [
|
await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [
|
||||||
existingMenuIds,
|
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(` ✅ 메뉴 삭제 완료: ${existingMenus.length}개`);
|
||||||
|
|
||||||
logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨");
|
logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨");
|
||||||
|
|
@ -2416,12 +2371,8 @@ export class MenuCopyService {
|
||||||
return { copiedCount, ruleIdMap };
|
return { copiedCount, ruleIdMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
// 2. 대상 회사에 이미 존재하는 모든 채번 규칙 조회 (원본 ID + 새로 생성될 ID 모두 체크)
|
|
||||||
=======
|
|
||||||
// 2. 대상 회사에 이미 존재하는 채번 규칙 한 번에 조회
|
// 2. 대상 회사에 이미 존재하는 채번 규칙 한 번에 조회
|
||||||
const ruleIds = allRulesResult.rows.map((r) => r.rule_id);
|
const ruleIds = allRulesResult.rows.map((r) => r.rule_id);
|
||||||
>>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176
|
|
||||||
const existingRulesResult = await client.query(
|
const existingRulesResult = await client.query(
|
||||||
`SELECT rule_id FROM numbering_rules WHERE company_code = $1`,
|
`SELECT rule_id FROM numbering_rules WHERE company_code = $1`,
|
||||||
[targetCompanyCode]
|
[targetCompanyCode]
|
||||||
|
|
@ -2438,22 +2389,9 @@ export class MenuCopyService {
|
||||||
const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = [];
|
const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = [];
|
||||||
|
|
||||||
for (const rule of allRulesResult.rows) {
|
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)) {
|
if (existingRuleIds.has(rule.rule_id)) {
|
||||||
|
// 기존 규칙은 동일한 ID로 매핑
|
||||||
ruleIdMap.set(rule.rule_id, rule.rule_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로 연결 업데이트 필요
|
// 새 메뉴 ID로 연결 업데이트 필요
|
||||||
const newMenuObjid = menuIdMap.get(rule.menu_objid);
|
const newMenuObjid = menuIdMap.get(rule.menu_objid);
|
||||||
|
|
@ -2470,7 +2408,6 @@ export class MenuCopyService {
|
||||||
: rule.rule_id;
|
: rule.rule_id;
|
||||||
const newRuleId = `${targetCompanyCode}_${originalSuffix}`;
|
const newRuleId = `${targetCompanyCode}_${originalSuffix}`;
|
||||||
|
|
||||||
>>>>>>> 932eb288c6feae85cf7808513b4e1f0b7e709176
|
|
||||||
ruleIdMap.set(rule.rule_id, newRuleId);
|
ruleIdMap.set(rule.rule_id, newRuleId);
|
||||||
originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId });
|
originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId });
|
||||||
rulesToCopy.push({ ...rule, newRuleId });
|
rulesToCopy.push({ ...rule, newRuleId });
|
||||||
|
|
|
||||||
|
|
@ -319,14 +319,17 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
if (componentId?.startsWith("unified-")) {
|
if (componentId?.startsWith("unified-")) {
|
||||||
const unifiedConfigPanels: Record<string, React.FC<{ config: any; onChange: (config: any) => void }>> = {
|
const unifiedConfigPanels: Record<string, React.FC<{ config: any; onChange: (config: any) => void }>> = {
|
||||||
"unified-input": require("@/components/unified/config-panels/UnifiedInputConfigPanel").UnifiedInputConfigPanel,
|
"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-date": require("@/components/unified/config-panels/UnifiedDateConfigPanel").UnifiedDateConfigPanel,
|
||||||
"unified-list": require("@/components/unified/config-panels/UnifiedListConfigPanel").UnifiedListConfigPanel,
|
"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-group": require("@/components/unified/config-panels/UnifiedGroupConfigPanel").UnifiedGroupConfigPanel,
|
||||||
"unified-media": require("@/components/unified/config-panels/UnifiedMediaConfigPanel").UnifiedMediaConfigPanel,
|
"unified-media": require("@/components/unified/config-panels/UnifiedMediaConfigPanel").UnifiedMediaConfigPanel,
|
||||||
"unified-biz": require("@/components/unified/config-panels/UnifiedBizConfigPanel").UnifiedBizConfigPanel,
|
"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];
|
const UnifiedConfigPanel = unifiedConfigPanels[componentId];
|
||||||
|
|
@ -1038,9 +1041,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 🆕 3.5. Unified 컴포넌트 - 반드시 다른 체크보다 먼저 처리
|
// 🆕 3.5. Unified 컴포넌트 - 반드시 다른 체크보다 먼저 처리
|
||||||
const unifiedComponentType =
|
const unifiedComponentType =
|
||||||
(selectedComponent as any).componentType ||
|
(selectedComponent as any).componentType || selectedComponent.componentConfig?.type || "";
|
||||||
selectedComponent.componentConfig?.type ||
|
|
||||||
"";
|
|
||||||
if (unifiedComponentType.startsWith("unified-")) {
|
if (unifiedComponentType.startsWith("unified-")) {
|
||||||
const configPanel = renderComponentConfigPanel();
|
const configPanel = renderComponentConfigPanel();
|
||||||
if (configPanel) {
|
if (configPanel) {
|
||||||
|
|
@ -1538,7 +1539,15 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-gray-200 p-2">
|
<div className="rounded-md border border-gray-200 p-2">
|
||||||
<ConditionalConfigPanel
|
<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) => {
|
onChange={(newConfig: ConditionalConfig) => {
|
||||||
handleUpdate("conditional", newConfig);
|
handleUpdate("conditional", newConfig);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UnifiedDate
|
* UnifiedDate
|
||||||
*
|
*
|
||||||
* 통합 날짜/시간 컴포넌트
|
* 통합 날짜/시간 컴포넌트
|
||||||
* - date: 날짜 선택
|
* - date: 날짜 선택
|
||||||
* - time: 시간 선택
|
* - time: 시간 선택
|
||||||
|
|
@ -37,14 +37,14 @@ const DATE_FORMATS: Record<string, string> = {
|
||||||
// 날짜 문자열 → Date 객체
|
// 날짜 문자열 → Date 객체
|
||||||
function parseDate(value: string | undefined, formatStr: string): Date | undefined {
|
function parseDate(value: string | undefined, formatStr: string): Date | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
|
|
||||||
const dateFnsFormat = DATE_FORMATS[formatStr] || formatStr;
|
const dateFnsFormat = DATE_FORMATS[formatStr] || formatStr;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ISO 형식 먼저 시도
|
// ISO 형식 먼저 시도
|
||||||
const isoDate = new Date(value);
|
const isoDate = new Date(value);
|
||||||
if (isValid(isoDate)) return isoDate;
|
if (isValid(isoDate)) return isoDate;
|
||||||
|
|
||||||
// 포맷에 맞게 파싱
|
// 포맷에 맞게 파싱
|
||||||
const parsed = parse(value, dateFnsFormat, new Date());
|
const parsed = parse(value, dateFnsFormat, new Date());
|
||||||
return isValid(parsed) ? parsed : undefined;
|
return isValid(parsed) ? parsed : undefined;
|
||||||
|
|
@ -63,151 +63,152 @@ function formatDate(date: Date | undefined, formatStr: string): string {
|
||||||
/**
|
/**
|
||||||
* 단일 날짜 선택 컴포넌트
|
* 단일 날짜 선택 컴포넌트
|
||||||
*/
|
*/
|
||||||
const SingleDatePicker = forwardRef<HTMLButtonElement, {
|
const SingleDatePicker = forwardRef<
|
||||||
value?: string;
|
HTMLButtonElement,
|
||||||
onChange?: (value: string) => void;
|
{
|
||||||
dateFormat: string;
|
value?: string;
|
||||||
showToday?: boolean;
|
onChange?: (value: string) => void;
|
||||||
minDate?: string;
|
dateFormat: string;
|
||||||
maxDate?: string;
|
showToday?: boolean;
|
||||||
disabled?: boolean;
|
minDate?: string;
|
||||||
readonly?: boolean;
|
maxDate?: string;
|
||||||
className?: string;
|
disabled?: boolean;
|
||||||
}>(({
|
readonly?: boolean;
|
||||||
value,
|
className?: string;
|
||||||
onChange,
|
}
|
||||||
dateFormat = "YYYY-MM-DD",
|
>(
|
||||||
showToday = true,
|
(
|
||||||
minDate,
|
{ value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className },
|
||||||
maxDate,
|
ref,
|
||||||
disabled,
|
) => {
|
||||||
readonly,
|
const [open, setOpen] = useState(false);
|
||||||
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 handleSelect = useCallback((selectedDate: Date | undefined) => {
|
const date = useMemo(() => parseDate(value, dateFormat), [value, dateFormat]);
|
||||||
if (selectedDate) {
|
const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]);
|
||||||
onChange?.(formatDate(selectedDate, 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);
|
setOpen(false);
|
||||||
}
|
}, [dateFormat, onChange]);
|
||||||
}, [dateFormat, onChange]);
|
|
||||||
|
|
||||||
const handleToday = useCallback(() => {
|
const handleClear = useCallback(() => {
|
||||||
onChange?.(formatDate(new Date(), dateFormat));
|
onChange?.("");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [dateFormat, onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
return (
|
||||||
onChange?.("");
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
setOpen(false);
|
<PopoverTrigger asChild>
|
||||||
}, [onChange]);
|
<Button
|
||||||
|
ref={ref}
|
||||||
return (
|
variant="outline"
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
disabled={disabled || readonly}
|
||||||
<PopoverTrigger asChild>
|
className={cn(
|
||||||
<Button
|
"h-10 w-full justify-start text-left font-normal",
|
||||||
ref={ref}
|
!value && "text-muted-foreground",
|
||||||
variant="outline"
|
className,
|
||||||
disabled={disabled || readonly}
|
)}
|
||||||
className={cn(
|
>
|
||||||
"h-10 w-full justify-start text-left font-normal",
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
!value && "text-muted-foreground",
|
{value || "날짜 선택"}
|
||||||
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}>
|
|
||||||
초기화
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</PopoverTrigger>
|
||||||
</PopoverContent>
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
</Popover>
|
<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";
|
SingleDatePicker.displayName = "SingleDatePicker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날짜 범위 선택 컴포넌트
|
* 날짜 범위 선택 컴포넌트
|
||||||
*/
|
*/
|
||||||
const RangeDatePicker = forwardRef<HTMLDivElement, {
|
const RangeDatePicker = forwardRef<
|
||||||
value?: [string, string];
|
HTMLDivElement,
|
||||||
onChange?: (value: [string, string]) => void;
|
{
|
||||||
dateFormat: string;
|
value?: [string, string];
|
||||||
minDate?: string;
|
onChange?: (value: [string, string]) => void;
|
||||||
maxDate?: string;
|
dateFormat: string;
|
||||||
disabled?: boolean;
|
minDate?: string;
|
||||||
readonly?: boolean;
|
maxDate?: string;
|
||||||
className?: string;
|
disabled?: boolean;
|
||||||
}>(({
|
readonly?: boolean;
|
||||||
value = ["", ""],
|
className?: string;
|
||||||
onChange,
|
}
|
||||||
dateFormat = "YYYY-MM-DD",
|
>(({ value = ["", ""], onChange, dateFormat = "YYYY-MM-DD", minDate, maxDate, disabled, readonly, className }, ref) => {
|
||||||
minDate,
|
|
||||||
maxDate,
|
|
||||||
disabled,
|
|
||||||
readonly,
|
|
||||||
className
|
|
||||||
}, ref) => {
|
|
||||||
const [openStart, setOpenStart] = useState(false);
|
const [openStart, setOpenStart] = useState(false);
|
||||||
const [openEnd, setOpenEnd] = useState(false);
|
const [openEnd, setOpenEnd] = useState(false);
|
||||||
|
|
||||||
const startDate = useMemo(() => parseDate(value[0], dateFormat), [value, dateFormat]);
|
const startDate = useMemo(() => parseDate(value[0], dateFormat), [value, dateFormat]);
|
||||||
const endDate = useMemo(() => parseDate(value[1], dateFormat), [value, dateFormat]);
|
const endDate = useMemo(() => parseDate(value[1], dateFormat), [value, dateFormat]);
|
||||||
const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]);
|
const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]);
|
||||||
const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]);
|
const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]);
|
||||||
|
|
||||||
const handleStartSelect = useCallback((date: Date | undefined) => {
|
const handleStartSelect = useCallback(
|
||||||
if (date) {
|
(date: Date | undefined) => {
|
||||||
const newStart = formatDate(date, dateFormat);
|
if (date) {
|
||||||
// 시작일이 종료일보다 크면 종료일도 같이 변경
|
const newStart = formatDate(date, dateFormat);
|
||||||
if (endDate && date > endDate) {
|
// 시작일이 종료일보다 크면 종료일도 같이 변경
|
||||||
onChange?.([newStart, newStart]);
|
if (endDate && date > endDate) {
|
||||||
} else {
|
onChange?.([newStart, newStart]);
|
||||||
onChange?.([newStart, value[1]]);
|
} else {
|
||||||
|
onChange?.([newStart, value[1]]);
|
||||||
|
}
|
||||||
|
setOpenStart(false);
|
||||||
}
|
}
|
||||||
setOpenStart(false);
|
},
|
||||||
}
|
[value, dateFormat, endDate, onChange],
|
||||||
}, [value, dateFormat, endDate, onChange]);
|
);
|
||||||
|
|
||||||
const handleEndSelect = useCallback((date: Date | undefined) => {
|
const handleEndSelect = useCallback(
|
||||||
if (date) {
|
(date: Date | undefined) => {
|
||||||
const newEnd = formatDate(date, dateFormat);
|
if (date) {
|
||||||
// 종료일이 시작일보다 작으면 시작일도 같이 변경
|
const newEnd = formatDate(date, dateFormat);
|
||||||
if (startDate && date < startDate) {
|
// 종료일이 시작일보다 작으면 시작일도 같이 변경
|
||||||
onChange?.([newEnd, newEnd]);
|
if (startDate && date < startDate) {
|
||||||
} else {
|
onChange?.([newEnd, newEnd]);
|
||||||
onChange?.([value[0], newEnd]);
|
} else {
|
||||||
|
onChange?.([value[0], newEnd]);
|
||||||
|
}
|
||||||
|
setOpenEnd(false);
|
||||||
}
|
}
|
||||||
setOpenEnd(false);
|
},
|
||||||
}
|
[value, dateFormat, startDate, onChange],
|
||||||
}, [value, dateFormat, startDate, onChange]);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn("flex items-center gap-2", className)}>
|
<div ref={ref} className={cn("flex items-center gap-2", className)}>
|
||||||
|
|
@ -217,10 +218,7 @@ const RangeDatePicker = forwardRef<HTMLDivElement, {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
className={cn(
|
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[0] && "text-muted-foreground")}
|
||||||
"h-10 flex-1 justify-start text-left font-normal",
|
|
||||||
!value[0] && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
{value[0] || "시작일"}
|
{value[0] || "시작일"}
|
||||||
|
|
@ -250,10 +248,7 @@ const RangeDatePicker = forwardRef<HTMLDivElement, {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
className={cn(
|
className={cn("h-10 flex-1 justify-start text-left font-normal", !value[1] && "text-muted-foreground")}
|
||||||
"h-10 flex-1 justify-start text-left font-normal",
|
|
||||||
!value[1] && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
{value[1] || "종료일"}
|
{value[1] || "종료일"}
|
||||||
|
|
@ -284,16 +279,19 @@ RangeDatePicker.displayName = "RangeDatePicker";
|
||||||
/**
|
/**
|
||||||
* 시간 선택 컴포넌트
|
* 시간 선택 컴포넌트
|
||||||
*/
|
*/
|
||||||
const TimePicker = forwardRef<HTMLInputElement, {
|
const TimePicker = forwardRef<
|
||||||
value?: string;
|
HTMLInputElement,
|
||||||
onChange?: (value: string) => void;
|
{
|
||||||
disabled?: boolean;
|
value?: string;
|
||||||
readonly?: boolean;
|
onChange?: (value: string) => void;
|
||||||
className?: string;
|
disabled?: boolean;
|
||||||
}>(({ value, onChange, disabled, readonly, className }, ref) => {
|
readonly?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
>(({ value, onChange, disabled, readonly, className }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative", className)}>
|
<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
|
<Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="time"
|
type="time"
|
||||||
|
|
@ -311,25 +309,19 @@ TimePicker.displayName = "TimePicker";
|
||||||
/**
|
/**
|
||||||
* 날짜+시간 선택 컴포넌트
|
* 날짜+시간 선택 컴포넌트
|
||||||
*/
|
*/
|
||||||
const DateTimePicker = forwardRef<HTMLDivElement, {
|
const DateTimePicker = forwardRef<
|
||||||
value?: string;
|
HTMLDivElement,
|
||||||
onChange?: (value: string) => void;
|
{
|
||||||
dateFormat: string;
|
value?: string;
|
||||||
minDate?: string;
|
onChange?: (value: string) => void;
|
||||||
maxDate?: string;
|
dateFormat: string;
|
||||||
disabled?: boolean;
|
minDate?: string;
|
||||||
readonly?: boolean;
|
maxDate?: string;
|
||||||
className?: string;
|
disabled?: boolean;
|
||||||
}>(({
|
readonly?: boolean;
|
||||||
value,
|
className?: string;
|
||||||
onChange,
|
}
|
||||||
dateFormat = "YYYY-MM-DD HH:mm",
|
>(({ value, onChange, dateFormat = "YYYY-MM-DD HH:mm", minDate, maxDate, disabled, readonly, className }, ref) => {
|
||||||
minDate,
|
|
||||||
maxDate,
|
|
||||||
disabled,
|
|
||||||
readonly,
|
|
||||||
className
|
|
||||||
}, ref) => {
|
|
||||||
// 날짜와 시간 분리
|
// 날짜와 시간 분리
|
||||||
const [datePart, timePart] = useMemo(() => {
|
const [datePart, timePart] = useMemo(() => {
|
||||||
if (!value) return ["", ""];
|
if (!value) return ["", ""];
|
||||||
|
|
@ -337,15 +329,21 @@ const DateTimePicker = forwardRef<HTMLDivElement, {
|
||||||
return [parts[0] || "", parts[1] || ""];
|
return [parts[0] || "", parts[1] || ""];
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleDateChange = useCallback((newDate: string) => {
|
const handleDateChange = useCallback(
|
||||||
const newValue = `${newDate} ${timePart || "00:00"}`;
|
(newDate: string) => {
|
||||||
onChange?.(newValue.trim());
|
const newValue = `${newDate} ${timePart || "00:00"}`;
|
||||||
}, [timePart, onChange]);
|
onChange?.(newValue.trim());
|
||||||
|
},
|
||||||
|
[timePart, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
const handleTimeChange = useCallback((newTime: string) => {
|
const handleTimeChange = useCallback(
|
||||||
const newValue = `${datePart || format(new Date(), "yyyy-MM-dd")} ${newTime}`;
|
(newTime: string) => {
|
||||||
onChange?.(newValue.trim());
|
const newValue = `${datePart || format(new Date(), "yyyy-MM-dd")} ${newTime}`;
|
||||||
}, [datePart, onChange]);
|
onChange?.(newValue.trim());
|
||||||
|
},
|
||||||
|
[datePart, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn("flex gap-2", className)}>
|
<div ref={ref} className={cn("flex gap-2", className)}>
|
||||||
|
|
@ -361,12 +359,7 @@ const DateTimePicker = forwardRef<HTMLDivElement, {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-32">
|
<div className="w-32">
|
||||||
<TimePicker
|
<TimePicker value={timePart} onChange={handleTimeChange} disabled={disabled} readonly={readonly} />
|
||||||
value={timePart}
|
|
||||||
onChange={handleTimeChange}
|
|
||||||
disabled={disabled}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -376,36 +369,64 @@ DateTimePicker.displayName = "DateTimePicker";
|
||||||
/**
|
/**
|
||||||
* 메인 UnifiedDate 컴포넌트
|
* 메인 UnifiedDate 컴포넌트
|
||||||
*/
|
*/
|
||||||
export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>(
|
export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>((props, ref) => {
|
||||||
(props, ref) => {
|
const { id, label, required, readonly, disabled, style, size, config: configProp, value, onChange } = props;
|
||||||
const {
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
readonly,
|
|
||||||
disabled,
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
config: configProp,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// config가 없으면 기본값 사용
|
// config가 없으면 기본값 사용
|
||||||
const config = configProp || { type: "date" as const };
|
const config = configProp || { type: "date" as const };
|
||||||
|
|
||||||
const dateFormat = config.format || "YYYY-MM-DD";
|
const dateFormat = config.format || "YYYY-MM-DD";
|
||||||
|
|
||||||
// 타입별 컴포넌트 렌더링
|
// 타입별 컴포넌트 렌더링
|
||||||
const renderDatePicker = () => {
|
const renderDatePicker = () => {
|
||||||
const isDisabled = disabled || readonly;
|
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 (
|
return (
|
||||||
<RangeDatePicker
|
<SingleDatePicker
|
||||||
value={Array.isArray(value) ? value as [string, string] : ["", ""]}
|
value={typeof value === "string" ? value : ""}
|
||||||
onChange={onChange as (value: [string, string]) => void}
|
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}
|
dateFormat={dateFormat}
|
||||||
minDate={config.minDate}
|
minDate={config.minDate}
|
||||||
maxDate={config.maxDate}
|
maxDate={config.maxDate}
|
||||||
|
|
@ -413,99 +434,55 @@ export const UnifiedDate = forwardRef<HTMLDivElement, UnifiedDateProps>(
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 타입별 렌더링
|
default:
|
||||||
switch (config.type) {
|
return (
|
||||||
case "date":
|
<SingleDatePicker
|
||||||
return (
|
value={typeof value === "string" ? value : ""}
|
||||||
<SingleDatePicker
|
onChange={(v) => onChange?.(v)}
|
||||||
value={typeof value === "string" ? value : ""}
|
dateFormat={dateFormat}
|
||||||
onChange={(v) => onChange?.(v)}
|
showToday={config.showToday}
|
||||||
dateFormat={dateFormat}
|
disabled={isDisabled}
|
||||||
showToday={config.showToday}
|
readonly={readonly}
|
||||||
minDate={config.minDate}
|
/>
|
||||||
maxDate={config.maxDate}
|
);
|
||||||
disabled={isDisabled}
|
}
|
||||||
readonly={readonly}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "time":
|
const showLabel = label && style?.labelDisplay !== false;
|
||||||
return (
|
const componentWidth = size?.width || style?.width;
|
||||||
<TimePicker
|
const componentHeight = size?.height || style?.height;
|
||||||
value={typeof value === "string" ? value : ""}
|
|
||||||
onChange={(v) => onChange?.(v)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "datetime":
|
return (
|
||||||
return (
|
<div
|
||||||
<DateTimePicker
|
ref={ref}
|
||||||
value={typeof value === "string" ? value : ""}
|
id={id}
|
||||||
onChange={(v) => onChange?.(v)}
|
className="flex flex-col"
|
||||||
dateFormat={dateFormat}
|
style={{
|
||||||
minDate={config.minDate}
|
width: componentWidth,
|
||||||
maxDate={config.maxDate}
|
height: componentHeight,
|
||||||
disabled={isDisabled}
|
}}
|
||||||
readonly={readonly}
|
>
|
||||||
/>
|
{showLabel && (
|
||||||
);
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
default:
|
style={{
|
||||||
return (
|
fontSize: style?.labelFontSize,
|
||||||
<SingleDatePicker
|
color: style?.labelColor,
|
||||||
value={typeof value === "string" ? value : ""}
|
fontWeight: style?.labelFontWeight,
|
||||||
onChange={(v) => onChange?.(v)}
|
marginBottom: style?.labelMarginBottom,
|
||||||
dateFormat={dateFormat}
|
}}
|
||||||
showToday={config.showToday}
|
className="flex-shrink-0 text-sm font-medium"
|
||||||
disabled={isDisabled}
|
>
|
||||||
readonly={readonly}
|
{label}
|
||||||
/>
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||||
);
|
</Label>
|
||||||
}
|
)}
|
||||||
};
|
<div className="min-h-0 flex-1">{renderDatePicker()}</div>
|
||||||
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
UnifiedDate.displayName = "UnifiedDate";
|
UnifiedDate.displayName = "UnifiedDate";
|
||||||
|
|
||||||
export default UnifiedDate;
|
export default UnifiedDate;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ interface UnifiedInputConfigPanelProps {
|
||||||
onChange: (config: Record<string, any>) => void;
|
onChange: (config: Record<string, any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({
|
export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = ({ config, onChange }) => {
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
// 설정 업데이트 핸들러
|
// 설정 업데이트 핸들러
|
||||||
const updateConfig = (field: string, value: any) => {
|
const updateConfig = (field: string, value: any) => {
|
||||||
onChange({ ...config, [field]: value });
|
onChange({ ...config, [field]: value });
|
||||||
|
|
@ -54,10 +51,7 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
|
||||||
{(config.inputType === "text" || !config.inputType) && (
|
{(config.inputType === "text" || !config.inputType) && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">입력 형식</Label>
|
<Label className="text-xs font-medium">입력 형식</Label>
|
||||||
<Select
|
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
|
||||||
value={config.format || "none"}
|
|
||||||
onValueChange={(value) => updateConfig("format", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="형식 선택" />
|
<SelectValue placeholder="형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -147,9 +141,7 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
|
||||||
placeholder="예: ###-####-####"
|
placeholder="예: ###-####-####"
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-muted-foreground text-[10px]"># = 숫자, A = 문자, * = 모든 문자</p>
|
||||||
# = 숫자, A = 문자, * = 모든 문자
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -158,4 +150,3 @@ export const UnifiedInputConfigPanel: React.FC<UnifiedInputConfigPanelProps> = (
|
||||||
UnifiedInputConfigPanel.displayName = "UnifiedInputConfigPanel";
|
UnifiedInputConfigPanel.displayName = "UnifiedInputConfigPanel";
|
||||||
|
|
||||||
export default UnifiedInputConfigPanel;
|
export default UnifiedInputConfigPanel;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue