refactor(pop): 타임라인 라벨 범용화 + 상태 값 매핑 동적 배열 전환
설정 패널의 도메인 특화 라벨("공정")을 범용 라벨("하위 데이터/표시명")로
교체하고, 상태 값 매핑을 고정 4키 객체에서 동적 배열(statusMappings)로
전환하여 임의 개수의 워크플로우 상태를 지원한다.
[라벨 범용화]
- "공정 데이터 소스" → "하위 데이터 소스"
- "공정 테이블" → "하위 테이블"
- "공정명" → "표시명"
- "현재 공정 강조" → "현재 항목 강조"
- "전체 공정 모달" → "전체 목록 모달"
- cell-renderers 내 "공정" 텍스트 전부 범용 교체
[상태 값 매핑 동적 배열]
- types.ts: statusValues(고정 4키) → statusMappings(StatusValueMapping[])
TimelineStatusSemantic("pending"|"active"|"done"), StatusValueMapping 타입 추가
TimelineProcessStep에 semantic? 필드 추가
- PopCardListV2Config: StatusMappingsEditor 컴포넌트 신규
(행 추가/삭제 + 시맨틱 Select + 기본값 적용 버튼)
- PopCardListV2Component: resolveStatusMappings() 레거시 자동 변환 함수
injectProcessFlow 동적 맵 기반 정규화로 전환
- cell-renderers: TIMELINE_STATUS_STYLES → TIMELINE_SEMANTIC_STYLES
getTimelineStyle() + LEGACY_STATUS_TO_SEMANTIC 레거시 호환
completedCount/statusLabel/isAcceptable 모두 semantic 기반으로 전환
This commit is contained in:
parent
599b5a4426
commit
ed3707a681
|
|
@ -29,6 +29,7 @@ import type {
|
|||
TimelineProcessStep,
|
||||
TimelineDataSource,
|
||||
ActionButtonUpdate,
|
||||
StatusValueMapping,
|
||||
} from "../types";
|
||||
import { CARD_PRESET_SPECS, DEFAULT_CARD_IMAGE } from "../types";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
|
|
@ -63,6 +64,20 @@ function parseCartRow(dbRow: Record<string, unknown>): Record<string, unknown> {
|
|||
};
|
||||
}
|
||||
|
||||
// 레거시 statusValues(고정 4키 객체) → statusMappings(동적 배열) 자동 변환
|
||||
function resolveStatusMappings(src: TimelineDataSource): StatusValueMapping[] {
|
||||
if (src.statusMappings && src.statusMappings.length > 0) return src.statusMappings;
|
||||
|
||||
// 레거시 호환: 기존 statusValues 객체가 있으면 변환
|
||||
const sv = (src as Record<string, unknown>).statusValues as Record<string, string> | undefined;
|
||||
return [
|
||||
{ dbValue: sv?.waiting || "waiting", label: "대기", semantic: "pending" as const },
|
||||
{ dbValue: sv?.accepted || "accepted", label: "접수", semantic: "active" as const },
|
||||
{ dbValue: sv?.inProgress || "in_progress", label: "진행중", semantic: "active" as const },
|
||||
{ dbValue: sv?.completed || "completed", label: "완료", semantic: "done" as const },
|
||||
];
|
||||
}
|
||||
|
||||
interface PopCardListV2ComponentProps {
|
||||
config?: PopCardListV2Config;
|
||||
className?: string;
|
||||
|
|
@ -309,7 +324,7 @@ export function PopCardListV2Component({
|
|||
return undefined;
|
||||
}, [cardGrid?.cells]);
|
||||
|
||||
// 공정 데이터 조회 + __processFlow__ 가상 컬럼 주입
|
||||
// 하위 데이터 조회 + __processFlow__ 가상 컬럼 주입
|
||||
const injectProcessFlow = useCallback(async (
|
||||
fetchedRows: RowData[],
|
||||
src: TimelineDataSource,
|
||||
|
|
@ -318,11 +333,15 @@ export function PopCardListV2Component({
|
|||
const rowIds = fetchedRows.map((r) => String(r.id)).filter(Boolean);
|
||||
if (rowIds.length === 0) return fetchedRows;
|
||||
|
||||
const sv = src.statusValues || {};
|
||||
const waitingVal = sv.waiting || "waiting";
|
||||
const acceptedVal = sv.accepted || "accepted";
|
||||
const inProgressVal = sv.inProgress || "in_progress";
|
||||
const completedVal = sv.completed || "completed";
|
||||
// statusMappings 동적 배열 → dbValue-to-내부키 맵 구축
|
||||
// 레거시 statusValues 객체도 자동 변환
|
||||
const mappings = resolveStatusMappings(src);
|
||||
const dbToInternal = new Map<string, string>();
|
||||
const dbToSemantic = new Map<string, string>();
|
||||
for (const m of mappings) {
|
||||
dbToInternal.set(m.dbValue, m.dbValue);
|
||||
dbToSemantic.set(m.dbValue, m.semantic);
|
||||
}
|
||||
|
||||
const processResult = await dataApi.getTableData(src.processTable, {
|
||||
page: 1,
|
||||
|
|
@ -338,30 +357,31 @@ export function PopCardListV2Component({
|
|||
if (!fkValue || !rowIds.includes(fkValue)) continue;
|
||||
if (!processMap.has(fkValue)) processMap.set(fkValue, []);
|
||||
|
||||
const rawStatus = String(p[src.statusColumn] || waitingVal);
|
||||
let normalizedStatus = rawStatus;
|
||||
if (rawStatus === waitingVal) normalizedStatus = "waiting";
|
||||
else if (rawStatus === acceptedVal) normalizedStatus = "accepted";
|
||||
else if (rawStatus === inProgressVal) normalizedStatus = "in_progress";
|
||||
else if (rawStatus === completedVal) normalizedStatus = "completed";
|
||||
const rawStatus = String(p[src.statusColumn] || "");
|
||||
const normalizedStatus = dbToInternal.get(rawStatus) || rawStatus;
|
||||
const semantic = dbToSemantic.get(rawStatus) || "pending";
|
||||
|
||||
processMap.get(fkValue)!.push({
|
||||
seqNo: parseInt(String(p[src.seqColumn] || "0"), 10),
|
||||
processName: String(p[src.nameColumn] || ""),
|
||||
status: normalizedStatus,
|
||||
isCurrent: normalizedStatus === "in_progress" || normalizedStatus === "accepted",
|
||||
semantic: semantic as "pending" | "active" | "done",
|
||||
isCurrent: semantic === "active",
|
||||
});
|
||||
}
|
||||
|
||||
// isCurrent 보정: in_progress가 없으면 첫 waiting을 current로
|
||||
// isCurrent 보정: active가 없으면 첫 pending을 current로
|
||||
for (const [, steps] of processMap) {
|
||||
steps.sort((a, b) => a.seqNo - b.seqNo);
|
||||
const hasInProgress = steps.some((s) => s.status === "in_progress");
|
||||
if (!hasInProgress) {
|
||||
const firstWaiting = steps.find((s) => s.status === "waiting");
|
||||
if (firstWaiting) {
|
||||
const hasActive = steps.some((s) => s.isCurrent);
|
||||
if (!hasActive) {
|
||||
const firstPending = steps.find((s) => {
|
||||
const sem = dbToSemantic.get(s.status) || "pending";
|
||||
return sem === "pending";
|
||||
});
|
||||
if (firstPending) {
|
||||
steps.forEach((s) => { s.isCurrent = false; });
|
||||
firstWaiting.isCurrent = true;
|
||||
firstPending.isCurrent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ import type {
|
|||
V2CardClickAction,
|
||||
ActionButtonUpdate,
|
||||
TimelineDataSource,
|
||||
StatusValueMapping,
|
||||
TimelineStatusSemantic,
|
||||
} from "../types";
|
||||
import type { ButtonVariant } from "../pop-button";
|
||||
import {
|
||||
|
|
@ -1487,11 +1489,11 @@ function TimelineConfigEditor({
|
|||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[9px] font-medium text-muted-foreground">공정 데이터 소스</span>
|
||||
<span className="text-[9px] font-medium text-muted-foreground">하위 데이터 소스</span>
|
||||
|
||||
{/* 공정 테이블 선택 */}
|
||||
{/* 하위 테이블 선택 */}
|
||||
<div>
|
||||
<Label className="text-[9px] text-muted-foreground">공정 테이블</Label>
|
||||
<Label className="text-[9px] text-muted-foreground">하위 테이블</Label>
|
||||
<Popover open={tableOpen} onOpenChange={setTableOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" className="mt-0.5 h-7 w-full justify-between text-[10px] font-normal">
|
||||
|
|
@ -1526,7 +1528,7 @@ function TimelineConfigEditor({
|
|||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 매핑 (공정 테이블 선택 후) */}
|
||||
{/* 컬럼 매핑 (하위 테이블 선택 후) */}
|
||||
{src.processTable && processColumns.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] text-muted-foreground">컬럼 매핑</Label>
|
||||
|
|
@ -1552,7 +1554,7 @@ function TimelineConfigEditor({
|
|||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[8px] text-muted-foreground">공정명</span>
|
||||
<span className="text-[8px] text-muted-foreground">표시명</span>
|
||||
<Select value={src.nameColumn || "__none__"} onValueChange={(v) => updateSource({ nameColumn: v === "__none__" ? "" : v })}>
|
||||
<SelectTrigger className="h-6 text-[9px]"><SelectValue placeholder="선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -1575,30 +1577,12 @@ function TimelineConfigEditor({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 상태 값 매핑 */}
|
||||
{/* 상태 값 매핑 (동적 배열) */}
|
||||
{src.processTable && src.statusColumn && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] text-muted-foreground">상태 값 매핑</Label>
|
||||
<p className="text-[8px] text-muted-foreground">DB에 저장된 실제 상태 값을 입력하세요. 비워두면 기본값 사용.</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{([
|
||||
{ key: "waiting", label: "대기", def: "waiting" },
|
||||
{ key: "accepted", label: "접수", def: "accepted" },
|
||||
{ key: "inProgress", label: "진행중", def: "in_progress" },
|
||||
{ key: "completed", label: "완료", def: "completed" },
|
||||
] as const).map((item) => (
|
||||
<div key={item.key}>
|
||||
<span className="text-[8px] text-muted-foreground">{item.label}</span>
|
||||
<Input
|
||||
value={src.statusValues?.[item.key] || ""}
|
||||
placeholder={item.def}
|
||||
onChange={(e) => updateSource({ statusValues: { ...src.statusValues, [item.key]: e.target.value } })}
|
||||
className="h-6 text-[9px]"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<StatusMappingsEditor
|
||||
mappings={src.statusMappings || []}
|
||||
onChange={(mappings) => updateSource({ statusMappings: mappings })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 구분선 */}
|
||||
|
|
@ -1630,7 +1614,7 @@ function TimelineConfigEditor({
|
|||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Label className="w-20 shrink-0 text-[9px] text-muted-foreground">현재 공정 강조</Label>
|
||||
<Label className="w-20 shrink-0 text-[9px] text-muted-foreground">현재 항목 강조</Label>
|
||||
<Switch
|
||||
checked={cell.currentHighlight !== false}
|
||||
onCheckedChange={(v) => onUpdate({ currentHighlight: v })}
|
||||
|
|
@ -1642,12 +1626,97 @@ function TimelineConfigEditor({
|
|||
checked={cell.showDetailModal !== false}
|
||||
onCheckedChange={(v) => onUpdate({ showDetailModal: v })}
|
||||
/>
|
||||
<span className="text-[8px] text-muted-foreground">전체 공정 모달</span>
|
||||
<span className="text-[8px] text-muted-foreground">전체 목록 모달</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 상태 값 매핑 에디터 (동적 배열) =====
|
||||
|
||||
const SEMANTIC_OPTIONS: { value: TimelineStatusSemantic; label: string }[] = [
|
||||
{ value: "pending", label: "대기" },
|
||||
{ value: "active", label: "진행" },
|
||||
{ value: "done", label: "완료" },
|
||||
];
|
||||
|
||||
const DEFAULT_STATUS_MAPPINGS: StatusValueMapping[] = [
|
||||
{ dbValue: "waiting", label: "대기", semantic: "pending" },
|
||||
{ dbValue: "accepted", label: "접수", semantic: "active" },
|
||||
{ dbValue: "in_progress", label: "진행중", semantic: "active" },
|
||||
{ dbValue: "completed", label: "완료", semantic: "done" },
|
||||
];
|
||||
|
||||
function StatusMappingsEditor({
|
||||
mappings,
|
||||
onChange,
|
||||
}: {
|
||||
mappings: StatusValueMapping[];
|
||||
onChange: (mappings: StatusValueMapping[]) => void;
|
||||
}) {
|
||||
const addMapping = () => {
|
||||
onChange([...mappings, { dbValue: "", label: "", semantic: "pending" }]);
|
||||
};
|
||||
|
||||
const updateMapping = (index: number, partial: Partial<StatusValueMapping>) => {
|
||||
onChange(mappings.map((m, i) => (i === index ? { ...m, ...partial } : m)));
|
||||
};
|
||||
|
||||
const removeMapping = (index: number) => {
|
||||
onChange(mappings.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const applyDefaults = () => {
|
||||
onChange([...DEFAULT_STATUS_MAPPINGS]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[9px] text-muted-foreground">상태 값 매핑</Label>
|
||||
<div className="flex gap-1">
|
||||
{mappings.length === 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={applyDefaults} className="h-5 px-1.5 text-[9px]">
|
||||
기본값 적용
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={addMapping} className="h-5 px-1.5 text-[9px]">
|
||||
<Plus className="mr-0.5 h-3 w-3" />추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[8px] text-muted-foreground">DB 값, 화면 라벨, 의미(대기/진행/완료)를 매핑합니다.</p>
|
||||
{mappings.map((m, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<Input
|
||||
value={m.dbValue}
|
||||
onChange={(e) => updateMapping(i, { dbValue: e.target.value })}
|
||||
placeholder="DB 값"
|
||||
className="h-6 flex-1 text-[10px]"
|
||||
/>
|
||||
<Input
|
||||
value={m.label}
|
||||
onChange={(e) => updateMapping(i, { label: e.target.value })}
|
||||
placeholder="라벨"
|
||||
className="h-6 flex-1 text-[10px]"
|
||||
/>
|
||||
<Select value={m.semantic} onValueChange={(v) => updateMapping(i, { semantic: v as TimelineStatusSemantic })}>
|
||||
<SelectTrigger className="h-6 w-16 text-[10px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{SEMANTIC_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value} className="text-[10px]">{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeMapping(i)} className="h-5 w-5 p-0">
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 액션 버튼 에디터 =====
|
||||
|
||||
function ActionButtonsEditor({
|
||||
|
|
|
|||
|
|
@ -333,15 +333,17 @@ function StatusBadgeCell({ cell, row }: CellRendererProps) {
|
|||
const strValue = String(value || "");
|
||||
const mapped = cell.statusMap?.find((m) => m.value === strValue);
|
||||
|
||||
// 접수가능 자동 판별: work_order_process 기반
|
||||
// 직전 공정이 completed이고 현재 공정이 waiting이면 "접수가능"
|
||||
// 접수가능 자동 판별: 하위 데이터 기반
|
||||
// 직전 항목이 done이고 현재 항목이 pending이면 "접수가능"
|
||||
const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
|
||||
const isAcceptable = useMemo(() => {
|
||||
if (!processFlow || strValue !== "waiting") return false;
|
||||
const currentIdx = processFlow.findIndex((s) => s.isCurrent);
|
||||
if (currentIdx < 0) return false;
|
||||
if (currentIdx === 0) return true;
|
||||
return processFlow[currentIdx - 1]?.status === "completed";
|
||||
const prevStep = processFlow[currentIdx - 1];
|
||||
const prevSem = prevStep?.semantic || LEGACY_STATUS_TO_SEMANTIC[prevStep?.status || ""] || "pending";
|
||||
return prevSem === "done";
|
||||
}, [processFlow, strValue]);
|
||||
|
||||
if (isAcceptable) {
|
||||
|
|
@ -391,29 +393,25 @@ function StatusBadgeCell({ cell, row }: CellRendererProps) {
|
|||
|
||||
// ===== 10. timeline =====
|
||||
|
||||
const TIMELINE_STATUS_STYLES: Record<string, { chipBg: string; chipText: string; icon: React.ReactNode }> = {
|
||||
completed: {
|
||||
chipBg: "#10b981",
|
||||
chipText: "#ffffff",
|
||||
icon: <CheckCircle2 className="h-2.5 w-2.5" />,
|
||||
},
|
||||
in_progress: {
|
||||
chipBg: "#f59e0b",
|
||||
chipText: "#ffffff",
|
||||
icon: <CircleDot className="h-2.5 w-2.5 animate-pulse" />,
|
||||
},
|
||||
accepted: {
|
||||
chipBg: "#3b82f6",
|
||||
chipText: "#ffffff",
|
||||
icon: <Play className="h-2.5 w-2.5" />,
|
||||
},
|
||||
waiting: {
|
||||
chipBg: "#e2e8f0",
|
||||
chipText: "#64748b",
|
||||
icon: <Clock className="h-2.5 w-2.5" />,
|
||||
},
|
||||
type TimelineStyle = { chipBg: string; chipText: string; icon: React.ReactNode };
|
||||
|
||||
const TIMELINE_SEMANTIC_STYLES: Record<string, TimelineStyle> = {
|
||||
done: { chipBg: "#10b981", chipText: "#ffffff", icon: <CheckCircle2 className="h-2.5 w-2.5" /> },
|
||||
active: { chipBg: "#3b82f6", chipText: "#ffffff", icon: <CircleDot className="h-2.5 w-2.5 animate-pulse" /> },
|
||||
pending: { chipBg: "#e2e8f0", chipText: "#64748b", icon: <Clock className="h-2.5 w-2.5" /> },
|
||||
};
|
||||
|
||||
// 레거시 status 값 → semantic 매핑 (기존 데이터 호환)
|
||||
const LEGACY_STATUS_TO_SEMANTIC: Record<string, string> = {
|
||||
completed: "done", in_progress: "active", accepted: "active", waiting: "pending",
|
||||
};
|
||||
|
||||
function getTimelineStyle(step: TimelineProcessStep): TimelineStyle {
|
||||
if (step.semantic) return TIMELINE_SEMANTIC_STYLES[step.semantic] || TIMELINE_SEMANTIC_STYLES.pending;
|
||||
const fallback = LEGACY_STATUS_TO_SEMANTIC[step.status];
|
||||
return TIMELINE_SEMANTIC_STYLES[fallback || "pending"];
|
||||
}
|
||||
|
||||
function TimelineCell({ cell, row }: CellRendererProps) {
|
||||
const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
|
||||
|
||||
|
|
@ -433,8 +431,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
| { kind: "step"; step: TimelineProcessStep }
|
||||
| { kind: "count"; count: number; side: "before" | "after" };
|
||||
|
||||
// 현재 공정 기준으로 앞뒤 배분하여 축약
|
||||
// 예: 10공정 중 4번이 현재, maxVisible=5 → [2]...[3공정]...[●4공정]...[5공정]...[5]
|
||||
// 현재 항목 기준으로 앞뒤 배분하여 축약
|
||||
const displayItems = useMemo((): DisplayItem[] => {
|
||||
if (processFlow.length <= maxVisible) {
|
||||
return processFlow.map((s) => ({ kind: "step" as const, step: s }));
|
||||
|
|
@ -445,7 +442,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
// 숫자칩 2개를 제외한 나머지를 앞뒤로 배분 (priority에 따라 여분 슬롯 방향 결정)
|
||||
const slotForSteps = maxVisible - 2;
|
||||
const half = Math.floor(slotForSteps / 2);
|
||||
const extra = slotForSteps - half - 1; // -1은 현재 공정
|
||||
const extra = slotForSteps - half - 1;
|
||||
const beforeSlots = priority === "before" ? Math.max(half, extra) : Math.min(half, extra);
|
||||
const afterSlots = slotForSteps - beforeSlots - 1;
|
||||
|
||||
|
|
@ -481,7 +478,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const completedCount = processFlow.filter((s) => s.status === "completed").length;
|
||||
const completedCount = processFlow.filter((s) => (s.semantic || LEGACY_STATUS_TO_SEMANTIC[s.status]) === "done").length;
|
||||
const totalCount = processFlow.length;
|
||||
|
||||
return (
|
||||
|
|
@ -493,7 +490,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
cell.align === "center" ? "justify-center" : cell.align === "right" ? "justify-end" : "justify-start",
|
||||
)}
|
||||
onClick={cell.showDetailModal !== false ? (e) => { e.stopPropagation(); setModalOpen(true); } : undefined}
|
||||
title={cell.showDetailModal !== false ? "클릭하여 전체 공정 현황 보기" : undefined}
|
||||
title={cell.showDetailModal !== false ? "클릭하여 전체 현황 보기" : undefined}
|
||||
>
|
||||
{displayItems.map((item, idx) => {
|
||||
const isLast = idx === displayItems.length - 1;
|
||||
|
|
@ -503,7 +500,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
<React.Fragment key={`cnt-${item.side}`}>
|
||||
<div
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-bold text-muted-foreground"
|
||||
title={item.side === "before" ? `이전 ${item.count}개 공정` : `이후 ${item.count}개 공정`}
|
||||
title={item.side === "before" ? `이전 ${item.count}개` : `이후 ${item.count}개`}
|
||||
>
|
||||
{item.count}
|
||||
</div>
|
||||
|
|
@ -512,7 +509,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const styles = TIMELINE_STATUS_STYLES[item.step.status] || TIMELINE_STATUS_STYLES.waiting;
|
||||
const styles = getTimelineStyle(item.step);
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.step.seqNo}>
|
||||
|
|
@ -540,20 +537,17 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">전체 공정 현황</DialogTitle>
|
||||
<DialogTitle className="text-base sm:text-lg">전체 현황</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
총 {totalCount}개 공정 중 {completedCount}개 완료
|
||||
총 {totalCount}개 중 {completedCount}개 완료
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-0">
|
||||
{processFlow.map((step, idx) => {
|
||||
const styles = TIMELINE_STATUS_STYLES[step.status] || TIMELINE_STATUS_STYLES.waiting;
|
||||
const statusLabel =
|
||||
step.status === "completed" ? "완료" :
|
||||
step.status === "in_progress" ? "진행중" :
|
||||
step.status === "accepted" ? "접수" :
|
||||
step.status === "hold" ? "보류" : "대기";
|
||||
const styles = getTimelineStyle(step);
|
||||
const sem = step.semantic || LEGACY_STATUS_TO_SEMANTIC[step.status] || "pending";
|
||||
const statusLabel = sem === "done" ? "완료" : sem === "active" ? "진행" : "대기";
|
||||
|
||||
return (
|
||||
<div key={step.seqNo} className="flex items-center">
|
||||
|
|
@ -569,7 +563,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
|
|||
{idx < processFlow.length - 1 && <div className="h-3 w-px bg-border" />}
|
||||
</div>
|
||||
|
||||
{/* 공정 정보 */}
|
||||
{/* 항목 정보 */}
|
||||
<div className={cn(
|
||||
"ml-2 flex flex-1 items-center justify-between rounded-md px-3 py-2",
|
||||
step.isCurrent && "bg-primary/5 ring-1 ring-primary/30",
|
||||
|
|
@ -630,7 +624,9 @@ function ActionButtonsCell({ cell, row, onActionButtonClick }: CellRendererProps
|
|||
const currentIdx = processFlow.findIndex((s) => s.isCurrent);
|
||||
if (currentIdx < 0) return false;
|
||||
if (currentIdx === 0) return true;
|
||||
return processFlow[currentIdx - 1]?.status === "completed";
|
||||
const prevStep = processFlow[currentIdx - 1];
|
||||
const prevSem = prevStep?.semantic || LEGACY_STATUS_TO_SEMANTIC[prevStep?.status || ""] || "pending";
|
||||
return prevSem === "done";
|
||||
}, [processFlow, statusValue]);
|
||||
|
||||
const effectiveStatus = isAcceptable ? "acceptable" : statusValue;
|
||||
|
|
|
|||
|
|
@ -743,27 +743,33 @@ export type CardCellType =
|
|||
| "action-buttons"
|
||||
| "footer-status";
|
||||
|
||||
// timeline 셀에서 사용하는 공정 단계 데이터
|
||||
// timeline 셀에서 사용하는 하위 단계 데이터
|
||||
export interface TimelineProcessStep {
|
||||
seqNo: number;
|
||||
processName: string;
|
||||
status: string;
|
||||
status: string; // DB 원본 값
|
||||
semantic?: "pending" | "active" | "done"; // 시각적 의미 (렌더러 색상 결정)
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
// timeline/status-badge/action-buttons가 참조하는 공정 테이블 설정
|
||||
// timeline/status-badge/action-buttons가 참조하는 하위 테이블 설정
|
||||
export interface TimelineDataSource {
|
||||
processTable: string; // 공정 데이터 테이블명 (예: work_order_process)
|
||||
processTable: string; // 하위 데이터 테이블명 (예: work_order_process)
|
||||
foreignKey: string; // 메인 테이블 id와 매칭되는 FK 컬럼 (예: wo_id)
|
||||
seqColumn: string; // 순서 컬럼 (예: seq_no)
|
||||
nameColumn: string; // 공정명 컬럼 (예: process_name)
|
||||
nameColumn: string; // 표시명 컬럼 (예: process_name)
|
||||
statusColumn: string; // 상태 컬럼 (예: status)
|
||||
statusValues?: { // 상태 값 매핑 (미설정 시 기본값 사용)
|
||||
waiting?: string; // 대기 (기본: "waiting")
|
||||
accepted?: string; // 접수 (기본: "accepted")
|
||||
inProgress?: string; // 진행중 (기본: "in_progress")
|
||||
completed?: string; // 완료 (기본: "completed")
|
||||
};
|
||||
// 상태 값 매핑: DB값 → 시맨틱 (동적 배열, 순서대로 표시)
|
||||
// 레거시 호환: 기존 { waiting, accepted, inProgress, completed } 객체도 런타임에서 자동 변환
|
||||
statusMappings?: StatusValueMapping[];
|
||||
}
|
||||
|
||||
export type TimelineStatusSemantic = "pending" | "active" | "done";
|
||||
|
||||
export interface StatusValueMapping {
|
||||
dbValue: string; // DB에 저장된 실제 값
|
||||
label: string; // 화면에 보이는 이름
|
||||
semantic: TimelineStatusSemantic; // 타임라인 색상 결정 (pending=회색, active=파랑, done=초록)
|
||||
}
|
||||
|
||||
export interface CardCellDefinitionV2 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue