feat: 기간별 단가 설정 기능 구현 - 자동 계산 시스템

- 선택항목 상세입력 컴포넌트 확장
  - 실시간 가격 계산 기능 추가 (할인율/할인금액, 반올림 방식)
  - 카테고리 값 기반 연산 매핑 시스템
  - 3단계 드릴다운 방식 설정 UI (메뉴 → 카테고리 → 값 매핑)

- 설정 가능한 계산 로직
  - autoCalculation 설정으로 계산 필드명 동적 지정
  - valueMapping으로 카테고리 코드와 연산 타입 매핑
  - 할인 방식: none/rate/amount
  - 반올림 방식: none/round/floor/ceil
  - 반올림 단위: 1/10/100/1000

- UI 개선
  - 입력 필드 가로 배치 (반응형 Grid)
  - 카테고리 타입 필드 옵션 로딩 개선
  - 계산 결과 필드 자동 표시 및 읽기 전용 처리
  - 날짜 입력 필드 네이티브 피커 지원

- API 연동
  - 2레벨 메뉴 목록 조회
  - 메뉴별 카테고리 컬럼 조회
  - 카테고리별 값 목록 조회

- 문서화
  - 기간별 단가 설정 가이드 작성
This commit is contained in:
kjs
2025-11-18 16:12:47 +09:00
parent 967b76591b
commit e1a5befdf7
10 changed files with 1966 additions and 186 deletions
+55 -52
View File
@@ -15,6 +15,7 @@ import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen";
import { toast } from "sonner";
import { useAuth } from "@/hooks/useAuth";
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
interface ScreenModalState {
isOpen: boolean;
@@ -394,60 +395,62 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
</div>
) : screenData ? (
<div
className="relative bg-white mx-auto"
style={{
width: `${screenDimensions?.width || 800}px`,
height: `${screenDimensions?.height || 600}px`,
transformOrigin: "center center",
}}
>
{screenData.components.map((component) => {
// 화면 관리 해상도를 사용하는 경우 offset 조정 불필요
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
<TableOptionsProvider>
<div
className="relative bg-white mx-auto"
style={{
width: `${screenDimensions?.width || 800}px`,
height: `${screenDimensions?.height || 600}px`,
transformOrigin: "center center",
}}
>
{screenData.components.map((component) => {
// 화면 관리 해상도를 사용하는 경우 offset 조정 불필요
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
...component,
position: {
...component.position,
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
},
};
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
...component,
position: {
...component.position,
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
},
};
return (
<InteractiveScreenViewerDynamic
key={component.id}
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
// console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
// console.log("📋 현재 formData:", formData);
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
// console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
return newFormData;
});
}}
onRefresh={() => {
// 부모 화면의 테이블 새로고침 이벤트 발송
console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송");
window.dispatchEvent(new CustomEvent("refreshTable"));
}}
screenInfo={{
id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName,
}}
/>
);
})}
</div>
return (
<InteractiveScreenViewerDynamic
key={component.id}
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
// console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
// console.log("📋 현재 formData:", formData);
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
// console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
return newFormData;
});
}}
onRefresh={() => {
// 부모 화면의 테이블 새로고침 이벤트 발송
console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송");
window.dispatchEvent(new CustomEvent("refreshTable"));
}}
screenInfo={{
id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName,
}}
/>
);
})}
</div>
</TableOptionsProvider>
) : (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground"> .</p>