; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
This commit is contained in:
leeheejin
2025-11-05 13:10:25 +09:00
21 changed files with 874 additions and 196 deletions

View File

@@ -109,9 +109,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
if (!prev) return null;
return {
...prev,
parts: prev.parts
.filter((part) => part.id !== partId)
.map((part, index) => ({ ...part, order: index + 1 })),
parts: prev.parts.filter((part) => part.id !== partId).map((part, index) => ({ ...part, order: index + 1 })),
};
});
@@ -132,7 +130,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
setLoading(true);
try {
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
let response;
if (existing) {
response = await updateNumberingRule(currentRule.ruleId, currentRule);
@@ -170,29 +168,32 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`);
}, []);
const handleDeleteSavedRule = useCallback(async (ruleId: string) => {
setLoading(true);
try {
const response = await deleteNumberingRule(ruleId);
if (response.success) {
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
if (selectedRuleId === ruleId) {
setSelectedRuleId(null);
setCurrentRule(null);
const handleDeleteSavedRule = useCallback(
async (ruleId: string) => {
setLoading(true);
try {
const response = await deleteNumberingRule(ruleId);
if (response.success) {
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
if (selectedRuleId === ruleId) {
setSelectedRuleId(null);
setCurrentRule(null);
}
toast.success("규칙이 삭제되었습니다");
} else {
toast.error(response.error || "삭제 실패");
}
toast.success("규칙이 삭제되었습니다");
} else {
toast.error(response.error || "삭제 실패");
} catch (error: any) {
toast.error(`삭제 실패: ${error.message}`);
} finally {
setLoading(false);
}
} catch (error: any) {
toast.error(`삭제 실패: ${error.message}`);
} finally {
setLoading(false);
}
}, [selectedRuleId]);
},
[selectedRuleId],
);
const handleNewRule = useCallback(() => {
const newRule: NumberingRuleConfig = {
@@ -207,7 +208,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
setSelectedRuleId(newRule.ruleId);
setCurrentRule(newRule);
toast.success("새 규칙이 생성되었습니다");
}, []);
@@ -228,35 +229,29 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
) : (
<h2 className="text-sm font-semibold sm:text-base">{leftTitle}</h2>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setEditingLeftTitle(true)}
>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingLeftTitle(true)}>
<Edit2 className="h-3 w-3" />
</Button>
</div>
<Button onClick={handleNewRule} variant="outline" className="h-9 w-full text-sm">
<Plus className="mr-2 h-4 w-4" />
<Plus className="mr-2 h-4 w-4" />
</Button>
<div className="flex-1 space-y-2 overflow-y-auto">
{loading ? (
<div className="flex h-32 items-center justify-center">
<p className="text-xs text-muted-foreground"> ...</p>
<p className="text-muted-foreground text-xs"> ...</p>
</div>
) : savedRules.length === 0 ? (
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-muted/50">
<p className="text-xs text-muted-foreground"> </p>
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
<p className="text-muted-foreground text-xs"> </p>
</div>
) : (
savedRules.map((rule) => (
<Card
key={rule.ruleId}
className={`cursor-pointer border-border transition-colors hover:bg-accent ${
className={`border-border hover:bg-accent cursor-pointer transition-colors ${
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
}`}
onClick={() => handleSelectRule(rule)}
@@ -265,9 +260,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
<p className="mt-1 text-xs text-muted-foreground">
{rule.parts.length}
</p>
<p className="text-muted-foreground mt-1 text-xs"> {rule.parts.length}</p>
</div>
<Button
variant="ghost"
@@ -278,7 +271,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
handleDeleteSavedRule(rule.ruleId);
}}
>
<Trash2 className="h-3 w-3 text-destructive" />
<Trash2 className="text-destructive h-3 w-3" />
</Button>
</div>
</CardHeader>
@@ -292,19 +285,15 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
</div>
{/* 구분선 */}
<div className="h-full w-px bg-border"></div>
<div className="bg-border h-full w-px"></div>
{/* 우측: 편집 영역 */}
<div className="flex flex-1 flex-col gap-4">
{!currentRule ? (
<div className="flex h-full flex-col items-center justify-center">
<div className="text-center">
<p className="mb-2 text-lg font-medium text-muted-foreground">
</p>
<p className="text-sm text-muted-foreground">
</p>
<p className="text-muted-foreground mb-2 text-lg font-medium"> </p>
<p className="text-muted-foreground text-sm"> </p>
</div>
</div>
) : (
@@ -322,12 +311,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
) : (
<h2 className="text-sm font-semibold sm:text-base">{rightTitle}</h2>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setEditingRightTitle(true)}
>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingRightTitle(true)}>
<Edit2 className="h-3 w-3" />
</Button>
</div>
@@ -336,9 +320,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<Label className="text-sm font-medium"></Label>
<Input
value={currentRule.ruleName}
onChange={(e) =>
setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))
}
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
className="h-9"
placeholder="예: 프로젝트 코드"
/>
@@ -348,9 +330,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<Label className="text-sm font-medium"> </Label>
<Select
value={currentRule.scopeType || "global"}
onValueChange={(value: "global" | "menu") =>
setCurrentRule((prev) => ({ ...prev!, scopeType: value }))
}
onValueChange={(value: "global" | "menu") => setCurrentRule((prev) => ({ ...prev!, scopeType: value }))}
disabled={isPreview}
>
<SelectTrigger className="h-9">
@@ -361,9 +341,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<SelectItem value="menu"></SelectItem>
</SelectContent>
</Select>
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
{currentRule.scopeType === "menu"
? "이 규칙이 설정된 상위 메뉴의 모든 하위 메뉴에서 사용 가능합니다"
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
{currentRule.scopeType === "menu"
? "⚠️ 현재 화면이 속한 2레벨 메뉴와 그 하위 메뉴(3레벨 이상)에서 사용됩니다. 형제 메뉴와 구분하여 채번 규칙을 관리할 때 유용합니다."
: "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}
</p>
</div>
@@ -380,16 +360,14 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<div className="flex-1 overflow-y-auto">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
<span className="text-xs text-muted-foreground">
<span className="text-muted-foreground text-xs">
{currentRule.parts.length}/{maxRules}
</span>
</div>
{currentRule.parts.length === 0 ? (
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-muted/50">
<p className="text-xs text-muted-foreground sm:text-sm">
</p>
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
<p className="text-muted-foreground text-xs sm:text-sm"> </p>
</div>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
@@ -416,11 +394,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
<Plus className="mr-2 h-4 w-4" />
</Button>
<Button
onClick={handleSave}
disabled={isPreview || loading}
className="h-9 flex-1 text-sm"
>
<Button onClick={handleSave} disabled={isPreview || loading} className="h-9 flex-1 text-sm">
<Save className="mr-2 h-4 w-4" />
{loading ? "저장 중..." : "저장"}
</Button>

View File

@@ -84,7 +84,7 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
// 자동값 생성 함수
const generateAutoValue = useCallback(
(autoValueType: string): string => {
async (autoValueType: string, ruleId?: string): Promise<string> => {
const now = new Date();
switch (autoValueType) {
case "current_datetime":
@@ -99,6 +99,20 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
return crypto.randomUUID();
case "sequence":
return `SEQ_${Date.now()}`;
case "numbering_rule":
// 채번 규칙 사용
if (ruleId) {
try {
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
const response = await generateNumberingCode(ruleId);
if (response.success && response.data) {
return response.data.generatedCode;
}
} catch (error) {
console.error("채번 규칙 코드 생성 실패:", error);
}
}
return "";
default:
return "";
}
@@ -129,24 +143,32 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
// 자동값 설정
useEffect(() => {
const widgetComponents = allComponents.filter((c) => c.type === "widget") as WidgetComponent[];
const autoValueUpdates: Record<string, any> = {};
const loadAutoValues = async () => {
const autoValueUpdates: Record<string, any> = {};
for (const widget of widgetComponents) {
const fieldName = widget.columnName || widget.id;
const currentValue = finalFormData[fieldName];
for (const widget of widgetComponents) {
const fieldName = widget.columnName || widget.id;
const currentValue = finalFormData[fieldName];
// 자동값이 설정되어 있고 현재 값이 없는 경우
if (widget.inputType === "auto" && widget.autoValueType && !currentValue) {
const autoValue = generateAutoValue(widget.autoValueType);
if (autoValue) {
autoValueUpdates[fieldName] = autoValue;
// 자동값이 설정되어 있고 현재 값이 없는 경우
if (widget.inputType === "auto" && widget.autoValueType && !currentValue) {
const autoValue = await generateAutoValue(
widget.autoValueType,
(widget as any).numberingRuleId // 채번 규칙 ID
);
if (autoValue) {
autoValueUpdates[fieldName] = autoValue;
}
}
}
}
if (Object.keys(autoValueUpdates).length > 0) {
setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates }));
}
if (Object.keys(autoValueUpdates).length > 0) {
setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates }));
}
};
loadAutoValues();
}, [allComponents, finalFormData, generateAutoValue]);
// 향상된 저장 핸들러

View File

@@ -136,7 +136,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
: null;
// 자동값 생성 함수
const generateAutoValue = useCallback((autoValueType: string): string => {
const generateAutoValue = useCallback(async (autoValueType: string, ruleId?: string): Promise<string> => {
const now = new Date();
switch (autoValueType) {
case "current_datetime":
@@ -152,6 +152,20 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
return crypto.randomUUID();
case "sequence":
return `SEQ_${Date.now()}`;
case "numbering_rule":
// 채번 규칙 사용
if (ruleId) {
try {
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
const response = await generateNumberingCode(ruleId);
if (response.success && response.data) {
return response.data.generatedCode;
}
} catch (error) {
console.error("채번 규칙 코드 생성 실패:", error);
}
}
return "";
default:
return "";
}

View File

@@ -610,16 +610,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16)));
const snappedX = padding + columnIndex * (columnWidth + (gap || 16));
// Y 좌표는 20px 단위로 스냅
// Y 좌표는 10px 단위로 스냅
const effectiveY = newComp.position.y - padding;
const rowIndex = Math.round(effectiveY / 20);
const snappedY = padding + rowIndex * 20;
const rowIndex = Math.round(effectiveY / 10);
const snappedY = padding + rowIndex * 10;
// 크기도 외부 격자와 동일하게 스냅
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
const snappedHeight = Math.max(40, Math.round(newComp.size.height / 20) * 20);
const snappedHeight = Math.max(10, Math.round(newComp.size.height / 10) * 10);
newComp.position = {
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보

View File

@@ -961,27 +961,27 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<div className="col-span-2">
<Label htmlFor="height" className="text-sm font-medium">
(40px )
(10px )
</Label>
<div className="mt-1 flex items-center space-x-2">
<Input
id="height"
type="number"
min="1"
max="20"
value={Math.round((localInputs.height || 40) / 40)}
max="100"
value={Math.round((localInputs.height || 10) / 10)}
onChange={(e) => {
const rows = Math.max(1, Math.min(20, Number(e.target.value)));
const newHeight = rows * 40;
const units = Math.max(1, Math.min(100, Number(e.target.value)));
const newHeight = units * 10;
setLocalInputs((prev) => ({ ...prev, height: newHeight.toString() }));
onUpdateProperty("size.height", newHeight);
}}
className="flex-1"
/>
<span className="text-sm text-gray-500"> = {localInputs.height || 40}px</span>
<span className="text-sm text-gray-500"> = {localInputs.height || 10}px</span>
</div>
<p className="mt-1 text-xs text-gray-500">
1 = 40px ( {Math.round((localInputs.height || 40) / 40)}) -
1 = 10px ( {Math.round((localInputs.height || 10) / 10)}) -
</p>
</div>
</>

View File

@@ -364,11 +364,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.size?.height || 0}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
const roundedValue = Math.max(40, Math.round(value / 40) * 40);
const roundedValue = Math.max(10, Math.round(value / 10) * 10);
handleUpdate("size.height", roundedValue);
}}
step={40}
placeholder="40"
step={10}
placeholder="10"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}

View File

@@ -7,6 +7,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { TextTypeConfig } from "@/types/screen";
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
interface TextTypeConfigPanelProps {
config: TextTypeConfig;
@@ -26,9 +28,14 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
autoInput: false,
autoValueType: "current_datetime" as const,
customValue: "",
numberingRuleId: "",
...config,
};
// 채번 규칙 목록 상태
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingRules, setLoadingRules] = useState(false);
// 로컬 상태로 실시간 입력 관리
const [localValues, setLocalValues] = useState({
minLength: safeConfig.minLength?.toString() || "",
@@ -41,8 +48,33 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
autoInput: safeConfig.autoInput,
autoValueType: safeConfig.autoValueType,
customValue: safeConfig.customValue,
numberingRuleId: safeConfig.numberingRuleId,
});
// 채번 규칙 목록 로드
useEffect(() => {
const loadRules = async () => {
setLoadingRules(true);
try {
// TODO: 현재 메뉴 objid를 화면 정보에서 가져와야 함
// 지금은 menuObjid 없이 호출 (global 규칙만 조회)
const response = await getAvailableNumberingRules();
if (response.success && response.data) {
setNumberingRules(response.data);
}
} catch (error) {
console.error("채번 규칙 목록 로드 실패:", error);
} finally {
setLoadingRules(false);
}
};
// autoValueType이 numbering_rule일 때만 로드
if (localValues.autoValueType === "numbering_rule") {
loadRules();
}
}, [localValues.autoValueType]);
// config가 변경될 때 로컬 상태 동기화
useEffect(() => {
setLocalValues({
@@ -56,6 +88,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
autoInput: safeConfig.autoInput,
autoValueType: safeConfig.autoValueType,
customValue: safeConfig.customValue,
numberingRuleId: safeConfig.numberingRuleId,
});
}, [
safeConfig.minLength,
@@ -68,6 +101,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
safeConfig.autoInput,
safeConfig.autoValueType,
safeConfig.customValue,
safeConfig.numberingRuleId,
]);
const updateConfig = (key: keyof TextTypeConfig, value: any) => {
@@ -90,16 +124,10 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
autoInput: key === "autoInput" ? value : localValues.autoInput,
autoValueType: key === "autoValueType" ? value : localValues.autoValueType,
customValue: key === "customValue" ? value : localValues.customValue,
numberingRuleId: key === "numberingRuleId" ? value : localValues.numberingRuleId,
};
const newConfig = JSON.parse(JSON.stringify(currentValues));
// console.log("📝 TextTypeConfig 업데이트:", {
// key,
// value,
// oldConfig: safeConfig,
// newConfig,
// localValues,
// });
setTimeout(() => {
onConfigChange(newConfig);
@@ -236,11 +264,45 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
<SelectItem value="current_user"> </SelectItem>
<SelectItem value="uuid"> ID (UUID)</SelectItem>
<SelectItem value="sequence"></SelectItem>
<SelectItem value="numbering_rule"> </SelectItem>
<SelectItem value="custom"> </SelectItem>
</SelectContent>
</Select>
</div>
{localValues.autoValueType === "numbering_rule" && (
<div>
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
<span className="text-destructive">*</span>
</Label>
<Select
value={localValues.numberingRuleId}
onValueChange={(value) => updateConfig("numberingRuleId", value)}
disabled={loadingRules}
>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
</SelectTrigger>
<SelectContent>
{numberingRules.length === 0 ? (
<SelectItem value="no-rules" disabled>
</SelectItem>
) : (
numberingRules.map((rule) => (
<SelectItem key={rule.ruleId} value={rule.ruleId}>
{rule.ruleName} ({rule.ruleId})
</SelectItem>
))
)}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-[10px]">
</p>
</div>
)}
{localValues.autoValueType === "custom" && (
<div>
<Label htmlFor="customValue" className="text-sm font-medium">