From ed3707a681edf1b3971cad47da2964cd60cac5f9 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 10 Mar 2026 17:33:25 +0900 Subject: [PATCH] =?UTF-8?q?refactor(pop):=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EB=9D=BC=EB=B2=A8=20=EB=B2=94=EC=9A=A9=ED=99=94=20?= =?UTF-8?q?+=20=EC=83=81=ED=83=9C=20=EA=B0=92=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EB=B0=B0=EC=97=B4=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90=EC=9D=98=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8A=B9=ED=99=94=20=EB=9D=BC=EB=B2=A8("?= =?UTF-8?q?=EA=B3=B5=EC=A0=95")=EC=9D=84=20=EB=B2=94=EC=9A=A9=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8("=ED=95=98=EC=9C=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0/?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=AA=85")=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=83=81=ED=83=9C=20=EA=B0=92=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=EC=9D=84=20=EA=B3=A0=EC=A0=95=204=ED=82=A4?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=EC=97=90=EC=84=9C=20=EB=8F=99=EC=A0=81=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4(statusMappings)=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=ED=95=98=EC=97=AC=20=EC=9E=84=EC=9D=98=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=EC=9D=98=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20[=EB=9D=BC=EB=B2=A8=20=EB=B2=94=EC=9A=A9?= =?UTF-8?q?=ED=99=94]=20-=20"=EA=B3=B5=EC=A0=95=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=86=8C=EC=8A=A4"=20=E2=86=92=20"=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=86=8C=EC=8A=A4"?= =?UTF-8?q?=20-=20"=EA=B3=B5=EC=A0=95=20=ED=85=8C=EC=9D=B4=EB=B8=94"=20?= =?UTF-8?q?=E2=86=92=20"=ED=95=98=EC=9C=84=20=ED=85=8C=EC=9D=B4=EB=B8=94"?= =?UTF-8?q?=20-=20"=EA=B3=B5=EC=A0=95=EB=AA=85"=20=E2=86=92=20"=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=AA=85"=20-=20"=ED=98=84=EC=9E=AC=20=EA=B3=B5?= =?UTF-8?q?=EC=A0=95=20=EA=B0=95=EC=A1=B0"=20=E2=86=92=20"=ED=98=84?= =?UTF-8?q?=EC=9E=AC=20=ED=95=AD=EB=AA=A9=20=EA=B0=95=EC=A1=B0"=20-=20"?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EA=B3=B5=EC=A0=95=20=EB=AA=A8=EB=8B=AC"?= =?UTF-8?q?=20=E2=86=92=20"=EC=A0=84=EC=B2=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC"=20-=20cell-renderers=20=EB=82=B4=20"?= =?UTF-8?q?=EA=B3=B5=EC=A0=95"=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=84?= =?UTF-8?q?=EB=B6=80=20=EB=B2=94=EC=9A=A9=20=EA=B5=90=EC=B2=B4=20[?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B0=92=20=EB=A7=A4=ED=95=91=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EB=B0=B0=EC=97=B4]=20-=20types.ts:=20statusValues(?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=204=ED=82=A4)=20=E2=86=92=20statusMappings(S?= =?UTF-8?q?tatusValueMapping[])=20=20=20TimelineStatusSemantic("pending"|"?= =?UTF-8?q?active"|"done"),=20StatusValueMapping=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=20=20TimelineProcessStep=EC=97=90=20seman?= =?UTF-8?q?tic=3F=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20PopCardL?= =?UTF-8?q?istV2Config:=20StatusMappingsEditor=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8B=A0=EA=B7=9C=20=20=20(=ED=96=89=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80/=EC=82=AD=EC=A0=9C=20+=20=EC=8B=9C=EB=A7=A8?= =?UTF-8?q?=ED=8B=B1=20Select=20+=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B2=84=ED=8A=BC)=20-=20PopCardListV2Com?= =?UTF-8?q?ponent:=20resolveStatusMappings()=20=EB=A0=88=EA=B1=B0=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=EB=B3=80=ED=99=98=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=20=20injectProcessFlow=20=EB=8F=99=EC=A0=81=20=EB=A7=B5=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=A0=95=EA=B7=9C=ED=99=94=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20-=20cell-renderers:=20TIMELINE=5FSTATUS=5F?= =?UTF-8?q?STYLES=20=E2=86=92=20TIMELINE=5FSEMANTIC=5FSTYLES=20=20=20getTi?= =?UTF-8?q?melineStyle()=20+=20LEGACY=5FSTATUS=5FTO=5FSEMANTIC=20=EB=A0=88?= =?UTF-8?q?=EA=B1=B0=EC=8B=9C=20=ED=98=B8=ED=99=98=20=20=20completedCount/?= =?UTF-8?q?statusLabel/isAcceptable=20=EB=AA=A8=EB=91=90=20semantic=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopCardListV2Component.tsx | 58 +++++--- .../pop-card-list-v2/PopCardListV2Config.tsx | 129 ++++++++++++++---- .../pop-card-list-v2/cell-renderers.tsx | 78 +++++------ frontend/lib/registry/pop-components/types.ts | 28 ++-- 4 files changed, 192 insertions(+), 101 deletions(-) diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index e6d16fda..eacb0ca6 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -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): Record { }; } +// 레거시 statusValues(고정 4키 객체) → statusMappings(동적 배열) 자동 변환 +function resolveStatusMappings(src: TimelineDataSource): StatusValueMapping[] { + if (src.statusMappings && src.statusMappings.length > 0) return src.statusMappings; + + // 레거시 호환: 기존 statusValues 객체가 있으면 변환 + const sv = (src as Record).statusValues as Record | 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(); + const dbToSemantic = new Map(); + 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; } } } diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx index a24d6402..6b37b569 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx @@ -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 (
- 공정 데이터 소스 + 하위 데이터 소스 - {/* 공정 테이블 선택 */} + {/* 하위 테이블 선택 */}
- +
- {/* 컬럼 매핑 (공정 테이블 선택 후) */} + {/* 컬럼 매핑 (하위 테이블 선택 후) */} {src.processTable && processColumns.length > 0 && (
@@ -1552,7 +1554,7 @@ function TimelineConfigEditor({
- 공정명 + 표시명 updateSource({ statusValues: { ...src.statusValues, [item.key]: e.target.value } })} - className="h-6 text-[9px]" - /> -
- ))} -
- + updateSource({ statusMappings: mappings })} + /> )} {/* 구분선 */} @@ -1630,7 +1614,7 @@ function TimelineConfigEditor({
- + onUpdate({ currentHighlight: v })} @@ -1642,12 +1626,97 @@ function TimelineConfigEditor({ checked={cell.showDetailModal !== false} onCheckedChange={(v) => onUpdate({ showDetailModal: v })} /> - 전체 공정 모달 + 전체 목록 모달
); } +// ===== 상태 값 매핑 에디터 (동적 배열) ===== + +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) => { + 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 ( +
+
+ +
+ {mappings.length === 0 && ( + + )} + +
+
+

DB 값, 화면 라벨, 의미(대기/진행/완료)를 매핑합니다.

+ {mappings.map((m, i) => ( +
+ updateMapping(i, { dbValue: e.target.value })} + placeholder="DB 값" + className="h-6 flex-1 text-[10px]" + /> + updateMapping(i, { label: e.target.value })} + placeholder="라벨" + className="h-6 flex-1 text-[10px]" + /> + + +
+ ))} +
+ ); +} + // ===== 액션 버튼 에디터 ===== function ActionButtonsEditor({ diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx index 259a6ac8..500af96e 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx @@ -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 = { - completed: { - chipBg: "#10b981", - chipText: "#ffffff", - icon: , - }, - in_progress: { - chipBg: "#f59e0b", - chipText: "#ffffff", - icon: , - }, - accepted: { - chipBg: "#3b82f6", - chipText: "#ffffff", - icon: , - }, - waiting: { - chipBg: "#e2e8f0", - chipText: "#64748b", - icon: , - }, +type TimelineStyle = { chipBg: string; chipText: string; icon: React.ReactNode }; + +const TIMELINE_SEMANTIC_STYLES: Record = { + done: { chipBg: "#10b981", chipText: "#ffffff", icon: }, + active: { chipBg: "#3b82f6", chipText: "#ffffff", icon: }, + pending: { chipBg: "#e2e8f0", chipText: "#64748b", icon: }, }; +// 레거시 status 값 → semantic 매핑 (기존 데이터 호환) +const LEGACY_STATUS_TO_SEMANTIC: Record = { + 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) {
{item.count}
@@ -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 ( @@ -540,20 +537,17 @@ function TimelineCell({ cell, row }: CellRendererProps) { - 전체 공정 현황 + 전체 현황 - 총 {totalCount}개 공정 중 {completedCount}개 완료 + 총 {totalCount}개 중 {completedCount}개 완료
{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 (
@@ -569,7 +563,7 @@ function TimelineCell({ cell, row }: CellRendererProps) { {idx < processFlow.length - 1 &&
}
- {/* 공정 정보 */} + {/* 항목 정보 */}
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; diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts index 1632821b..8d478ff3 100644 --- a/frontend/lib/registry/pop-components/types.ts +++ b/frontend/lib/registry/pop-components/types.ts @@ -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 {