Merge branch 'ycshin-node' of http://39.117.244.52:3000/kjs/ERP-node into mhkim-node
This commit is contained in:
@@ -458,6 +458,14 @@ select {
|
||||
border-color: hsl(var(--destructive)) !important;
|
||||
}
|
||||
|
||||
|
||||
/* 채번 세그먼트 포커스 스타일 (shadcn Input과 동일한 3단 구조) */
|
||||
.numbering-segment:focus-within {
|
||||
box-shadow: 0 0 0 3px hsl(var(--ring) / 0.5);
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
|
||||
.validation-error-msg-wrapper {
|
||||
height: 0;
|
||||
|
||||
@@ -288,6 +288,7 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [parentValue, setParentValue] = useState<CategoryValue | null>(null);
|
||||
const [continuousAdd, setContinuousAdd] = useState(false);
|
||||
const [editingValue, setEditingValue] = useState<CategoryValue | null>(null);
|
||||
const [deletingValue, setDeletingValue] = useState<CategoryValue | null>(null);
|
||||
|
||||
@@ -512,21 +513,24 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||
const response = await createCategoryValue(input);
|
||||
if (response.success) {
|
||||
toast.success("카테고리가 추가되었습니다");
|
||||
// 폼 초기화 (모달은 닫지 않고 연속 입력)
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
valueCode: "",
|
||||
valueLabel: "",
|
||||
description: "",
|
||||
color: "",
|
||||
}));
|
||||
setTimeout(() => addNameRef.current?.focus(), 50);
|
||||
// 기존 펼침 상태 유지하면서 데이터 새로고침
|
||||
await loadTree(true);
|
||||
// 부모 노드만 펼치기 (하위 추가 시)
|
||||
if (parentValue) {
|
||||
setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
|
||||
}
|
||||
|
||||
if (continuousAdd) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
valueCode: "",
|
||||
valueLabel: "",
|
||||
description: "",
|
||||
color: "",
|
||||
}));
|
||||
setTimeout(() => addNameRef.current?.focus(), 50);
|
||||
} else {
|
||||
setFormData({ valueCode: "", valueLabel: "", description: "", color: "", isActive: true });
|
||||
setIsAddModalOpen(false);
|
||||
}
|
||||
} else {
|
||||
toast.error(response.error || "추가 실패");
|
||||
}
|
||||
@@ -818,6 +822,19 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||
추가
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
<div className="border-t px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="tree-continuous-add"
|
||||
checked={continuousAdd}
|
||||
onCheckedChange={(checked) => setContinuousAdd(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="tree-continuous-add" className="cursor-pointer text-sm font-normal select-none">
|
||||
저장 후 계속 입력 (연속 등록 모드)
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -629,7 +629,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>((pro
|
||||
): SelectOption[] => {
|
||||
const result: SelectOption[] = [];
|
||||
for (const item of items) {
|
||||
const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
|
||||
const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
|
||||
result.push({
|
||||
value: String(item.valueId), // valueId를 value로 사용 (채번 매핑과 일치)
|
||||
label: prefix + item.valueLabel,
|
||||
|
||||
@@ -909,10 +909,10 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||
const templateSuffix = templateParts.length > 1 ? templateParts.slice(1).join("") : "";
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center rounded-md border">
|
||||
<div className="numbering-segment border-input flex h-full items-center rounded-md border outline-none transition-[color,box-shadow]">
|
||||
{/* 고정 접두어 */}
|
||||
{templatePrefix && (
|
||||
<span className="text-muted-foreground bg-muted flex h-full items-center px-2 text-sm">
|
||||
<span className="text-muted-foreground bg-muted flex h-full items-center rounded-l-[5px] px-2 text-sm">
|
||||
{templatePrefix}
|
||||
</span>
|
||||
)}
|
||||
@@ -945,13 +945,13 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||
}
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 bg-transparent px-2 text-sm focus-visible:outline-none"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm ring-0"
|
||||
disabled={disabled || isGeneratingNumbering}
|
||||
style={inputTextStyle}
|
||||
style={{ ...inputTextStyle, outline: 'none' }}
|
||||
/>
|
||||
{/* 고정 접미어 */}
|
||||
{templateSuffix && (
|
||||
<span className="text-muted-foreground bg-muted flex h-full items-center px-2 text-sm">
|
||||
<span className="text-muted-foreground bg-muted flex h-full items-center rounded-r-[5px] px-2 text-sm">
|
||||
{templateSuffix}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -901,7 +901,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>((props, ref) =
|
||||
): SelectOption[] => {
|
||||
const result: SelectOption[] = [];
|
||||
for (const item of items) {
|
||||
const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
|
||||
const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
|
||||
result.push({
|
||||
value: item.valueCode, // 🔧 valueCode를 value로 사용
|
||||
label: prefix + item.valueLabel,
|
||||
|
||||
@@ -98,6 +98,16 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||
return el instanceof HTMLSelectElement && el.hasAttribute("aria-hidden");
|
||||
}
|
||||
|
||||
// 복합 입력 필드(채번 세그먼트 등)의 시각적 테두리 컨테이너 탐지
|
||||
// input 자체에 border가 없고 부모가 border를 가진 경우 부모를 반환
|
||||
function findBorderContainer(input: TargetEl): HTMLElement {
|
||||
const parent = input.parentElement;
|
||||
if (parent && parent.classList.contains("border")) {
|
||||
return parent;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function isEmpty(input: TargetEl): boolean {
|
||||
if (input instanceof HTMLButtonElement) {
|
||||
// Radix Select: data-placeholder 속성이 자식 span에 있으면 미선택 상태
|
||||
@@ -120,20 +130,24 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||
}
|
||||
|
||||
function markError(input: TargetEl) {
|
||||
input.setAttribute(ERROR_ATTR, "true");
|
||||
const container = findBorderContainer(input);
|
||||
container.setAttribute(ERROR_ATTR, "true");
|
||||
errorFields.add(input);
|
||||
showErrorMsg(input);
|
||||
}
|
||||
|
||||
function clearError(input: TargetEl) {
|
||||
input.removeAttribute(ERROR_ATTR);
|
||||
const container = findBorderContainer(input);
|
||||
container.removeAttribute(ERROR_ATTR);
|
||||
errorFields.delete(input);
|
||||
removeErrorMsg(input);
|
||||
}
|
||||
|
||||
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
|
||||
// 복합 입력(채번 세그먼트 등)은 border 컨테이너 바깥에 삽입
|
||||
function showErrorMsg(input: TargetEl) {
|
||||
if (input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
|
||||
const container = findBorderContainer(input);
|
||||
if (container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = MSG_WRAPPER_CLASS;
|
||||
@@ -142,17 +156,19 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||
msg.textContent = "필수 입력 항목입니다";
|
||||
wrapper.appendChild(msg);
|
||||
|
||||
input.insertAdjacentElement("afterend", wrapper);
|
||||
container.insertAdjacentElement("afterend", wrapper);
|
||||
}
|
||||
|
||||
function removeErrorMsg(input: TargetEl) {
|
||||
const wrapper = input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
|
||||
const container = findBorderContainer(input);
|
||||
const wrapper = container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
|
||||
if (wrapper) wrapper.remove();
|
||||
}
|
||||
|
||||
function highlightField(input: TargetEl) {
|
||||
input.setAttribute(HIGHLIGHT_ATTR, "true");
|
||||
input.addEventListener("animationend", () => input.removeAttribute(HIGHLIGHT_ATTR), { once: true });
|
||||
const container = findBorderContainer(input);
|
||||
container.setAttribute(HIGHLIGHT_ATTR, "true");
|
||||
container.addEventListener("animationend", () => container.removeAttribute(HIGHLIGHT_ATTR), { once: true });
|
||||
|
||||
if (input instanceof HTMLButtonElement) {
|
||||
input.click();
|
||||
|
||||
Reference in New Issue
Block a user