style(pop-work-detail): ISA-101 디자인 토큰 도입 및 크기 수정
- DESIGN 상수 도입 (button, input, stat, section, sidebar, nav, infoBar, defectRow) - COLORS 상수 도입 (good, defect, complete, warning, info) - 버튼 높이: 44px → 56px (ISA-101 장갑 터치 기준) - 핵심 숫자(수량/실적): 18px → 36px (2-3m 가독성) - 섹션 제목/라벨: 14px → 16px (1m 가독성) - 입력 필드 높이: 44px → 56px - 불량 유형 행 높이: 40px → 56px - 사이드바 너비: 208px → 220px, 항목 패딩 확대 - 기능 변경 없음 (디자인만 수정)
This commit is contained in:
@@ -133,6 +133,29 @@ const DEFAULT_CFG: PopWorkDetailConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// ISA-101 디자인 토큰 (산업 터치 기준)
|
||||
// ========================================
|
||||
|
||||
const DESIGN = {
|
||||
button: { height: 56, minWidth: 100 },
|
||||
input: { height: 56 },
|
||||
stat: { valueSize: 36, labelSize: 14, weight: 700 },
|
||||
section: { titleSize: 16, gap: 20 },
|
||||
sidebar: { width: 220, itemPadding: '16px 20px' },
|
||||
nav: { height: 56 },
|
||||
infoBar: { labelSize: 14, valueSize: 16 },
|
||||
defectRow: { height: 56 },
|
||||
} as const;
|
||||
|
||||
const COLORS = {
|
||||
good: 'text-green-600',
|
||||
defect: 'text-red-600',
|
||||
complete: 'text-blue-600',
|
||||
warning: 'text-amber-600',
|
||||
info: 'text-violet-600',
|
||||
} as const;
|
||||
|
||||
// ========================================
|
||||
// Props
|
||||
// ========================================
|
||||
@@ -710,7 +733,7 @@ export function PopWorkDetailComponent({
|
||||
{/* 본문: 좌측 사이드바 + 우측 콘텐츠 */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* 좌측 사이드바 */}
|
||||
<div className="w-52 shrink-0 overflow-y-auto border-r bg-muted/30">
|
||||
<div className="shrink-0 overflow-y-auto border-r bg-muted/30" style={{ width: `${DESIGN.sidebar.width}px` }}>
|
||||
{(["PRE", "IN", "POST"] as WorkPhase[]).map((phase) => {
|
||||
const phaseGroups = groupsByPhase[phase];
|
||||
if (!phaseGroups || phaseGroups.length === 0) return null;
|
||||
@@ -723,11 +746,12 @@ export function PopWorkDetailComponent({
|
||||
<button
|
||||
key={g.itemId}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2.5 px-4 py-3 text-left transition-colors",
|
||||
"flex w-full items-center gap-2.5 text-left transition-colors",
|
||||
selectedGroupId === g.itemId
|
||||
? "bg-primary/10 text-primary border-l-2 border-primary"
|
||||
: "hover:bg-muted/60"
|
||||
)}
|
||||
style={{ padding: DESIGN.sidebar.itemPadding }}
|
||||
onClick={() => {
|
||||
setSelectedGroupId(g.itemId);
|
||||
setResultTabActive(false);
|
||||
@@ -736,7 +760,7 @@ export function PopWorkDetailComponent({
|
||||
>
|
||||
<StepStatusIcon status={g.stepStatus} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="block truncate text-sm font-medium leading-tight">
|
||||
<span className="block truncate font-medium leading-tight" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>
|
||||
{g.title}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
@@ -745,7 +769,7 @@ export function PopWorkDetailComponent({
|
||||
<span className="text-primary">진행중</span>
|
||||
)}
|
||||
{g.timer.completedAt && (
|
||||
<span className="text-green-600">완료</span>
|
||||
<span className={COLORS.good}>완료</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -761,11 +785,12 @@ export function PopWorkDetailComponent({
|
||||
<div className="mx-3 my-2 border-t" />
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2.5 px-4 py-3 text-left transition-colors",
|
||||
"flex w-full items-center gap-2.5 text-left transition-colors",
|
||||
resultTabActive
|
||||
? "bg-primary/10 text-primary border-l-2 border-primary"
|
||||
: "hover:bg-muted/60"
|
||||
)}
|
||||
style={{ padding: DESIGN.sidebar.itemPadding }}
|
||||
onClick={() => {
|
||||
setResultTabActive(true);
|
||||
setSelectedGroupId(null);
|
||||
@@ -774,7 +799,7 @@ export function PopWorkDetailComponent({
|
||||
>
|
||||
<ClipboardList className="h-4 w-4 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="block truncate text-sm font-medium leading-tight">
|
||||
<span className="block truncate font-medium leading-tight" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>
|
||||
실적 입력
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -818,15 +843,15 @@ export function PopWorkDetailComponent({
|
||||
|
||||
{cfg.showQuantityInput && !isProcessCompleted && !hasResultSections && (
|
||||
<div className="w-full max-w-sm space-y-4 rounded-lg border p-5">
|
||||
<p className="text-sm font-semibold">실적 수량 등록</p>
|
||||
<p className="font-semibold" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>실적 수량 등록</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-12 text-sm text-muted-foreground">양품</span>
|
||||
<Input type="number" className="h-11 text-base" value={goodQty} onChange={(e) => setGoodQty(e.target.value)} placeholder="0" />
|
||||
<span className="w-12 text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>양품</span>
|
||||
<Input type="number" className="text-base" style={{ height: `${DESIGN.input.height}px` }} value={goodQty} onChange={(e) => setGoodQty(e.target.value)} placeholder="0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-12 text-sm text-muted-foreground">불량</span>
|
||||
<Input type="number" className="h-11 text-base" value={defectQty} onChange={(e) => setDefectQty(e.target.value)} placeholder="0" />
|
||||
<span className="w-12 text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>불량</span>
|
||||
<Input type="number" className="text-base" style={{ height: `${DESIGN.input.height}px` }} value={defectQty} onChange={(e) => setDefectQty(e.target.value)} placeholder="0" />
|
||||
</div>
|
||||
{(parseInt(goodQty, 10) || 0) + (parseInt(defectQty, 10) || 0) > 0 && (
|
||||
<p className="text-right text-sm text-muted-foreground">
|
||||
@@ -839,7 +864,8 @@ export function PopWorkDetailComponent({
|
||||
|
||||
{!isProcessCompleted && cfg.navigation.showCompleteButton && (
|
||||
<Button
|
||||
className="h-12 gap-2 px-8 text-base"
|
||||
className="gap-2 px-8"
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
onClick={handleProcessComplete}
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5" />공정 완료
|
||||
@@ -912,7 +938,8 @@ export function PopWorkDetailComponent({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 gap-1.5 px-5 text-sm"
|
||||
className="gap-1.5 px-5"
|
||||
style={{ height: `${DESIGN.nav.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={currentItemIdx <= 0 && selectedIndex <= 0}
|
||||
onClick={() => {
|
||||
if (currentItemIdx > 0) {
|
||||
@@ -937,7 +964,8 @@ export function PopWorkDetailComponent({
|
||||
<Button
|
||||
size="sm"
|
||||
variant={currentItemIdx >= currentItems.length - 1 && selectedIndex >= groups.length - 1 ? "default" : "outline"}
|
||||
className="h-11 gap-1.5 px-5 text-sm"
|
||||
className="gap-1.5 px-5"
|
||||
style={{ height: `${DESIGN.nav.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
onClick={() => {
|
||||
if (currentItemIdx < currentItems.length - 1) {
|
||||
setCurrentItemIdx(currentItemIdx + 1);
|
||||
@@ -999,14 +1027,14 @@ export function PopWorkDetailComponent({
|
||||
<div className="flex items-center gap-3 px-4 py-2.5">
|
||||
<Package className="h-4.5 w-4.5 shrink-0 text-muted-foreground" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">양품</span>
|
||||
<Input type="number" className="h-10 w-24 text-sm" value={goodQty} onChange={(e) => setGoodQty(e.target.value)} disabled={isProcessCompleted} />
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>양품</span>
|
||||
<Input type="number" className="w-24" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={goodQty} onChange={(e) => setGoodQty(e.target.value)} disabled={isProcessCompleted} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">불량</span>
|
||||
<Input type="number" className="h-10 w-24 text-sm" value={defectQty} onChange={(e) => setDefectQty(e.target.value)} disabled={isProcessCompleted} />
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>불량</span>
|
||||
<Input type="number" className="w-24" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={defectQty} onChange={(e) => setDefectQty(e.target.value)} disabled={isProcessCompleted} />
|
||||
</div>
|
||||
<Button size="sm" variant="outline" className="h-10 px-4 text-sm" onClick={handleQuantityRegister} disabled={isProcessCompleted}>
|
||||
<Button size="sm" variant="outline" className="px-4" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={handleQuantityRegister} disabled={isProcessCompleted}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1017,7 +1045,8 @@ export function PopWorkDetailComponent({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 gap-1.5 px-5 text-sm"
|
||||
className="gap-1.5 px-5"
|
||||
style={{ height: `${DESIGN.nav.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={selectedIndex <= 0}
|
||||
onClick={() => navigateStep(-1)}
|
||||
>
|
||||
@@ -1032,7 +1061,8 @@ export function PopWorkDetailComponent({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 gap-1.5 px-5 text-sm"
|
||||
className="gap-1.5 px-5"
|
||||
style={{ height: `${DESIGN.nav.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
onClick={() => navigateStep(1)}
|
||||
>
|
||||
다음<ChevronRight className="h-4 w-4" />
|
||||
@@ -1041,7 +1071,8 @@ export function PopWorkDetailComponent({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="h-11 gap-1.5 px-5 text-sm"
|
||||
className="gap-1.5 px-5"
|
||||
style={{ height: `${DESIGN.nav.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
onClick={handleProcessComplete}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />공정 완료
|
||||
@@ -1292,33 +1323,33 @@ function ResultPanel({
|
||||
{/* 확정 상태 배너 */}
|
||||
{isConfirmed && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2.5">
|
||||
<FileCheck className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-700">실적이 확정되었습니다</span>
|
||||
<FileCheck className={cn("h-4 w-4", COLORS.good)} />
|
||||
<span className="font-medium text-green-700" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>실적이 확정되었습니다</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 공정 현황: 접수량 / 작업완료 / 잔여 + 앞공정 완료량 */}
|
||||
<div className="rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div className="mb-2 text-xs font-semibold uppercase text-muted-foreground">공정 현황</div>
|
||||
<div className="mb-2 font-semibold uppercase text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>공정 현황</div>
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">{inputQty}</div>
|
||||
<div className="text-xs text-muted-foreground">접수량</div>
|
||||
<div className="font-bold" style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{inputQty}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>접수량</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-blue-600">{accumulatedTotal}</div>
|
||||
<div className="text-xs text-muted-foreground">작업완료</div>
|
||||
<div className={COLORS.complete} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{accumulatedTotal}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>작업완료</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${remainingQty > 0 ? "text-amber-600" : "text-green-600"}`}>
|
||||
<div className={remainingQty > 0 ? COLORS.warning : COLORS.good} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>
|
||||
{remainingQty}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">잔여</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>잔여</div>
|
||||
</div>
|
||||
{availableInfo && availableInfo.availableQty > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-violet-600">{availableInfo.availableQty}</div>
|
||||
<div className="text-xs text-muted-foreground">추가접수가능</div>
|
||||
<div className={COLORS.info} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{availableInfo.availableQty}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>추가접수가능</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1340,23 +1371,23 @@ function ResultPanel({
|
||||
|
||||
{/* 누적 실적 현황 */}
|
||||
<div className="rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div className="mb-2 text-xs font-semibold uppercase text-muted-foreground">누적 실적</div>
|
||||
<div className="mb-2 font-semibold uppercase text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>누적 실적</div>
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">{accumulatedTotal}</div>
|
||||
<div className="text-xs text-muted-foreground">총생산</div>
|
||||
<div className="font-bold" style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{accumulatedTotal}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>총생산</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-600">{accumulatedGood}</div>
|
||||
<div className="text-xs text-muted-foreground">양품</div>
|
||||
<div className={COLORS.good} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{accumulatedGood}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>양품</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-red-600">{accumulatedDefect}</div>
|
||||
<div className="text-xs text-muted-foreground">불량</div>
|
||||
<div className={COLORS.defect} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{accumulatedDefect}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>불량</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">{history.length}</div>
|
||||
<div className="text-xs text-muted-foreground">차수</div>
|
||||
<div className="font-bold" style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}>{history.length}</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: `${DESIGN.stat.labelSize}px` }}>차수</div>
|
||||
</div>
|
||||
</div>
|
||||
{accumulatedTotal > 0 && (
|
||||
@@ -1372,21 +1403,22 @@ function ResultPanel({
|
||||
{/* 이번 차수 실적 입력 */}
|
||||
{!isConfirmed && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold">이번 차수 실적</div>
|
||||
<div className="font-semibold" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>이번 차수 실적</div>
|
||||
|
||||
{/* 생산수량 */}
|
||||
{enabledSections.some((s) => s.type === "total-qty") && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">생산수량</label>
|
||||
<label className="font-medium" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>생산수량</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-11 w-40 text-base"
|
||||
className="w-40 text-base"
|
||||
style={{ height: `${DESIGN.input.height}px` }}
|
||||
value={batchQty}
|
||||
onChange={(e) => setBatchQty(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">EA</span>
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>EA</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1394,23 +1426,25 @@ function ResultPanel({
|
||||
{/* 양품/불량 */}
|
||||
{enabledSections.some((s) => s.type === "good-defect") && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">양품 / 불량</label>
|
||||
<label className="font-medium" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>양품 / 불량</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">양품</span>
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>양품</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="h-11 w-28 bg-muted/50 text-base"
|
||||
className="w-28 bg-muted/50 text-base"
|
||||
style={{ height: `${DESIGN.input.height}px` }}
|
||||
value={batchGood > 0 ? String(batchGood) : ""}
|
||||
readOnly
|
||||
placeholder="자동"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">불량</span>
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>불량</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="h-11 w-28 text-base"
|
||||
className="w-28 text-base"
|
||||
style={{ height: `${DESIGN.input.height}px` }}
|
||||
value={batchDefect}
|
||||
onChange={(e) => setBatchDefect(e.target.value)}
|
||||
placeholder="0"
|
||||
@@ -1429,19 +1463,20 @@ function ResultPanel({
|
||||
{enabledSections.some((s) => s.type === "defect-types") && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">불량 유형 상세</label>
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={addDefectEntry}>
|
||||
<label className="font-medium" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>불량 유형 상세</label>
|
||||
<Button size="sm" variant="outline" className="gap-1" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.stat.labelSize}px` }} onClick={addDefectEntry}>
|
||||
<Plus className="h-3.5 w-3.5" />추가
|
||||
</Button>
|
||||
</div>
|
||||
{defectEntries.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">등록된 불량 유형이 없습니다.</p>
|
||||
<p className="text-muted-foreground" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>등록된 불량 유형이 없습니다.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{defectEntries.map((entry, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 rounded-lg border p-2.5">
|
||||
<select
|
||||
className="h-10 rounded-md border px-2 text-sm"
|
||||
className="rounded-md border px-2"
|
||||
style={{ height: `${DESIGN.defectRow.height}px`, fontSize: `${DESIGN.stat.labelSize}px` }}
|
||||
value={entry.defect_code}
|
||||
onChange={(e) => updateDefectEntry(idx, "defect_code", e.target.value)}
|
||||
>
|
||||
@@ -1454,13 +1489,15 @@ function ResultPanel({
|
||||
</select>
|
||||
<Input
|
||||
type="number"
|
||||
className="h-10 w-20 text-sm"
|
||||
className="w-20"
|
||||
style={{ height: `${DESIGN.defectRow.height}px`, fontSize: `${DESIGN.stat.labelSize}px` }}
|
||||
value={entry.qty}
|
||||
onChange={(e) => updateDefectEntry(idx, "qty", e.target.value)}
|
||||
placeholder="수량"
|
||||
/>
|
||||
<select
|
||||
className="h-10 rounded-md border px-2 text-sm"
|
||||
className="rounded-md border px-2"
|
||||
style={{ height: `${DESIGN.defectRow.height}px`, fontSize: `${DESIGN.stat.labelSize}px` }}
|
||||
value={entry.disposition}
|
||||
onChange={(e) => updateDefectEntry(idx, "disposition", e.target.value)}
|
||||
>
|
||||
@@ -1468,8 +1505,8 @@ function ResultPanel({
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={() => removeDefectEntry(idx)}>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
<Button size="icon" variant="ghost" className="shrink-0" style={{ height: `${DESIGN.defectRow.height}px`, width: `${DESIGN.defectRow.height}px` }} onClick={() => removeDefectEntry(idx)}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@@ -1481,7 +1518,7 @@ function ResultPanel({
|
||||
{/* 비고 */}
|
||||
{enabledSections.some((s) => s.type === "note") && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">비고</label>
|
||||
<label className="font-medium" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>비고</label>
|
||||
<Textarea
|
||||
className="min-h-[60px] text-sm"
|
||||
value={resultNote}
|
||||
@@ -1507,7 +1544,8 @@ function ResultPanel({
|
||||
{/* 등록 버튼 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
className="h-11 gap-2 px-6 text-sm"
|
||||
className="gap-2 px-6"
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
onClick={handleSubmitBatch}
|
||||
disabled={saving || !batchQty}
|
||||
>
|
||||
@@ -1520,7 +1558,7 @@ function ResultPanel({
|
||||
|
||||
{/* 이전 실적 이력 */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold">등록 이력</div>
|
||||
<div className="font-semibold" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>등록 이력</div>
|
||||
{historyLoading ? (
|
||||
<div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />불러오는 중...
|
||||
@@ -1534,9 +1572,9 @@ function ResultPanel({
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="secondary" className="text-xs px-2">#{h.seq}</Badge>
|
||||
<span className="text-sm font-medium">+{h.batch_qty}</span>
|
||||
<span className="text-xs text-green-600">양품 +{h.batch_good}</span>
|
||||
<span className={cn("text-xs", COLORS.good)}>양품 +{h.batch_good}</span>
|
||||
{h.batch_defect > 0 && (
|
||||
<span className="text-xs text-red-600">불량 +{h.batch_defect}</span>
|
||||
<span className={cn("text-xs", COLORS.defect)}>불량 +{h.batch_defect}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
@@ -1599,8 +1637,8 @@ function InfoBar({ fields, parentRow, processName }: InfoBarProps) {
|
||||
: parentRow[f.column];
|
||||
return (
|
||||
<div key={f.column} className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-muted-foreground">{f.label}</span>
|
||||
<span className="text-sm font-medium">{val != null ? String(val) : "-"}</span>
|
||||
<span className="text-muted-foreground" style={{ fontSize: `${DESIGN.infoBar.labelSize}px` }}>{f.label}</span>
|
||||
<span className="font-medium" style={{ fontSize: `${DESIGN.infoBar.valueSize}px` }}>{val != null ? String(val) : "-"}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1640,13 +1678,13 @@ function GroupTimerHeader({
|
||||
{/* 그룹 제목 + 진행 카운트 */}
|
||||
<div className="flex items-center justify-between px-4 pt-3 pb-1.5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-sm font-semibold">{group.title}</span>
|
||||
<span className="font-semibold" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>{group.title}</span>
|
||||
<Badge variant="secondary" className="text-xs px-2 py-0.5">
|
||||
{group.completed}/{group.total}
|
||||
</Badge>
|
||||
</div>
|
||||
{isGroupCompleted && (
|
||||
<Badge variant="outline" className="text-xs text-green-600 px-2 py-0.5">완료</Badge>
|
||||
<Badge variant="outline" className={cn("text-xs px-2 py-0.5", COLORS.good)}>완료</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1656,7 +1694,7 @@ function GroupTimerHeader({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Timer className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-mono text-sm font-medium tabular-nums">{groupTimerFormatted}</span>
|
||||
<span className="font-mono font-medium tabular-nums" style={{ fontSize: `${DESIGN.section.titleSize}px` }}>{groupTimerFormatted}</span>
|
||||
<span className="text-xs text-muted-foreground">작업</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
@@ -1667,26 +1705,26 @@ function GroupTimerHeader({
|
||||
{!isProcessCompleted && !isGroupCompleted && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{!isGroupStarted && (
|
||||
<Button size="sm" variant="outline" className="h-10 px-4 text-sm" onClick={() => onTimerAction("start")}>
|
||||
<Button size="sm" variant="outline" className="px-4" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={() => onTimerAction("start")}>
|
||||
<Play className="mr-1.5 h-4 w-4" />시작
|
||||
</Button>
|
||||
)}
|
||||
{isGroupStarted && !isGroupPaused && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-10 px-3 text-sm" onClick={() => onTimerAction("pause")}>
|
||||
<Button size="sm" variant="outline" className="px-3" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={() => onTimerAction("pause")}>
|
||||
<Pause className="mr-1.5 h-4 w-4" />정지
|
||||
</Button>
|
||||
<Button size="sm" variant="default" className="h-10 px-3 text-sm" onClick={() => onTimerAction("complete")}>
|
||||
<Button size="sm" variant="default" className="px-3" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={() => onTimerAction("complete")}>
|
||||
<CheckCircle2 className="mr-1.5 h-4 w-4" />완료
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isGroupStarted && isGroupPaused && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-10 px-3 text-sm" onClick={() => onTimerAction("resume")}>
|
||||
<Button size="sm" variant="outline" className="px-3" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={() => onTimerAction("resume")}>
|
||||
<Play className="mr-1.5 h-4 w-4" />재개
|
||||
</Button>
|
||||
<Button size="sm" variant="default" className="h-10 px-3 text-sm" onClick={() => onTimerAction("complete")}>
|
||||
<Button size="sm" variant="default" className="px-3" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={() => onTimerAction("complete")}>
|
||||
<CheckCircle2 className="mr-1.5 h-4 w-4" />완료
|
||||
</Button>
|
||||
</>
|
||||
@@ -1845,7 +1883,7 @@ function InspectNumeric({ item, disabled, saving, onSave }: { item: WorkResultRo
|
||||
<div className="mb-2 text-xs text-muted-foreground">{item.inspection_method}</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input type="number" className="h-10 w-36 text-sm" value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="측정값 입력" />
|
||||
<Input type="number" className="w-36" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="측정값 입력" />
|
||||
{item.unit && <span className="text-xs text-muted-foreground">{item.unit}</span>}
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{item.is_passed === "Y" && !saving && <Badge variant="outline" className="text-xs text-green-600">합격</Badge>}
|
||||
@@ -1877,7 +1915,8 @@ function InspectOX({ item, disabled, saving, onSave }: { item: WorkResultRow; di
|
||||
<Button
|
||||
size="sm"
|
||||
variant={currentValue === "OK" ? "default" : "outline"}
|
||||
className={cn("h-11 gap-1.5 px-6 text-sm font-medium", currentValue === "OK" && "bg-green-600 hover:bg-green-700")}
|
||||
className={cn("gap-1.5 px-6 font-medium", currentValue === "OK" && "bg-green-600 hover:bg-green-700")}
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={disabled}
|
||||
onClick={() => handleSelect("OK")}
|
||||
>
|
||||
@@ -1886,7 +1925,8 @@ function InspectOX({ item, disabled, saving, onSave }: { item: WorkResultRow; di
|
||||
<Button
|
||||
size="sm"
|
||||
variant={currentValue === "NG" ? "default" : "outline"}
|
||||
className={cn("h-11 gap-1.5 px-6 text-sm font-medium", currentValue === "NG" && "bg-red-600 hover:bg-red-700")}
|
||||
className={cn("gap-1.5 px-6 font-medium", currentValue === "NG" && "bg-red-600 hover:bg-red-700")}
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={disabled}
|
||||
onClick={() => handleSelect("NG")}
|
||||
>
|
||||
@@ -1933,7 +1973,8 @@ function InspectSelect({ item, disabled, saving, onSave }: { item: WorkResultRow
|
||||
key={opt}
|
||||
size="sm"
|
||||
variant={currentValue === opt ? "default" : "outline"}
|
||||
className="h-10 px-4 text-sm"
|
||||
className="px-4"
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={disabled}
|
||||
onClick={() => handleSelect(opt)}
|
||||
>
|
||||
@@ -1974,11 +2015,12 @@ function InspectText({ item, disabled, saving, onSave }: { item: WorkResultRow;
|
||||
{item.is_required === "Y" && <span className="text-xs font-semibold text-destructive">필수</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input className="h-10 flex-1 text-sm" value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="내용 입력" />
|
||||
<Input className="flex-1" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="내용 입력" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant={judged === "Y" ? "default" : "outline"}
|
||||
className={cn("h-10 px-4 text-sm", judged === "Y" && "bg-green-600 hover:bg-green-700")}
|
||||
className={cn("px-4", judged === "Y" && "bg-green-600 hover:bg-green-700")}
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={disabled}
|
||||
onClick={() => handleJudge("Y")}
|
||||
>
|
||||
@@ -1987,7 +2029,8 @@ function InspectText({ item, disabled, saving, onSave }: { item: WorkResultRow;
|
||||
<Button
|
||||
size="sm"
|
||||
variant={judged === "N" ? "default" : "outline"}
|
||||
className={cn("h-10 px-4 text-sm", judged === "N" && "bg-red-600 hover:bg-red-700")}
|
||||
className={cn("px-4", judged === "N" && "bg-red-600 hover:bg-red-700")}
|
||||
style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
disabled={disabled}
|
||||
onClick={() => handleJudge("N")}
|
||||
>
|
||||
@@ -2016,7 +2059,7 @@ function InputItem({ item, disabled, saving, onSave }: { item: WorkResultRow; di
|
||||
<div className={cn("rounded-lg border px-4 py-3", item.status === "completed" && "border-green-200 bg-green-50")}>
|
||||
<div className="mb-2 text-sm font-medium">{item.detail_label || item.detail_content}</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input type={inputType} className="h-10 flex-1 text-sm" value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="값 입력" />
|
||||
<Input type={inputType} className="flex-1" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={inputVal} onChange={(e) => setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="값 입력" />
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2084,14 +2127,15 @@ function MaterialItem({ item, disabled, saving, onSave }: { item: WorkResultRow;
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className="h-10 flex-1 text-sm"
|
||||
className="flex-1"
|
||||
style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }}
|
||||
value={inputVal}
|
||||
onChange={(e) => setInputVal(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
placeholder="LOT/바코드 스캔 또는 입력"
|
||||
/>
|
||||
<Button size="sm" variant="outline" className="h-10 px-4 text-sm" onClick={handleSubmit} disabled={disabled || !inputVal}>
|
||||
<Button size="sm" variant="outline" className="px-4" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={handleSubmit} disabled={disabled || !inputVal}>
|
||||
확인
|
||||
</Button>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
@@ -2133,11 +2177,11 @@ function ResultInputItem({ item, disabled, saving, onSave }: { item: WorkResultR
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">양품</span>
|
||||
<Input type="number" className="h-10 w-24 text-sm" value={good} onChange={(e) => setGood(e.target.value)} disabled={disabled} placeholder="0" />
|
||||
<Input type="number" className="w-24" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={good} onChange={(e) => setGood(e.target.value)} disabled={disabled} placeholder="0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">불량</span>
|
||||
<Input type="number" className="h-10 w-24 text-sm" value={defect} onChange={(e) => setDefect(e.target.value)} disabled={disabled} placeholder="0" />
|
||||
<Input type="number" className="w-24" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={defect} onChange={(e) => setDefect(e.target.value)} disabled={disabled} placeholder="0" />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">합계: {total}</span>
|
||||
</div>
|
||||
@@ -2145,17 +2189,17 @@ function ResultInputItem({ item, disabled, saving, onSave }: { item: WorkResultR
|
||||
{parseInt(defect, 10) > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">불량유형</span>
|
||||
<Input className="h-10 flex-1 text-sm" value={defectType} onChange={(e) => setDefectType(e.target.value)} disabled={disabled} placeholder="스크래치, 치수불량 등" />
|
||||
<Input className="flex-1" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={defectType} onChange={(e) => setDefectType(e.target.value)} disabled={disabled} placeholder="스크래치, 치수불량 등" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">비고</span>
|
||||
<Input className="h-10 flex-1 text-sm" value={note} onChange={(e) => setNote(e.target.value)} disabled={disabled} placeholder="비고 (선택)" />
|
||||
<Input className="flex-1" style={{ height: `${DESIGN.input.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} value={note} onChange={(e) => setNote(e.target.value)} disabled={disabled} placeholder="비고 (선택)" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Button size="sm" variant="outline" className="h-10 px-4 text-sm" onClick={handleSave} disabled={disabled}>
|
||||
<Button size="sm" variant="outline" className="px-4" style={{ height: `${DESIGN.button.height}px`, fontSize: `${DESIGN.section.titleSize}px` }} onClick={handleSave} disabled={disabled}>
|
||||
실적 등록
|
||||
</Button>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
|
||||
Reference in New Issue
Block a user