feat: [worker] 절곡 작업일지 이미지 R2 presigned URL 전환 + 품질검사 3건 수정
절곡 작업일지: - API bending_images 맵을 받아서 R2 presigned URL로 이미지 로드 - getBendingImageUrl()에 bendingImages 맵 조회 우선, API fallback 유지 - 4개 섹션(가이드레일, 하단마감재, 셔터박스, 연기차단재) 모두 적용 품질검사: - 요약카드 draft 상태 접수 건수 포함 - 검사완료 버튼 미검사/진행중 시 disabled - 완료 상태 수정 버튼 disabled(흐리게) + 편집 모드 진입 차단
This commit is contained in:
@@ -30,9 +30,10 @@ import { ProductionSummarySection } from './bending/ProductionSummarySection';
|
||||
interface BendingWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
lotNoMap?: Record<string, string>; // BD-{prefix}-{lengthCode} → LOT NO
|
||||
bendingImages?: Record<string, string>; // R2 presigned URL 맵
|
||||
}
|
||||
|
||||
export function BendingWorkLogContent({ data: order, lotNoMap }: BendingWorkLogContentProps) {
|
||||
export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: BendingWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -166,19 +167,23 @@ export function BendingWorkLogContent({ data: order, lotNoMap }: BendingWorkLogC
|
||||
mapping={mapping}
|
||||
lotNo={order.lotNo}
|
||||
lotNoMap={lotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<BottomBarSection
|
||||
bendingInfo={bendingInfo!}
|
||||
mapping={mapping}
|
||||
lotNoMap={lotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<ShutterBoxSection
|
||||
bendingInfo={bendingInfo!}
|
||||
lotNoMap={lotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<SmokeBarrierSection
|
||||
bendingInfo={bendingInfo!}
|
||||
lotNoMap={lotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -14,9 +14,10 @@ interface BottomBarSectionProps {
|
||||
bendingInfo: BendingInfoExtended;
|
||||
mapping: MaterialMapping;
|
||||
lotNoMap?: Record<string, string>;
|
||||
bendingImages?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function BottomBarSection({ bendingInfo, mapping, lotNoMap }: BottomBarSectionProps) {
|
||||
export function BottomBarSection({ bendingInfo, mapping, lotNoMap, bendingImages }: BottomBarSectionProps) {
|
||||
const rows = buildBottomBarRows(bendingInfo.bottomBar, mapping, bendingInfo.productCode);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
@@ -30,7 +31,7 @@ export function BottomBarSection({ bendingInfo, mapping, lotNoMap }: BottomBarSe
|
||||
{/* 좌측: 이미지 */}
|
||||
<div className="flex-shrink-0 w-48">
|
||||
<img
|
||||
src={getBendingImageUrl('bottombar', bendingInfo.productCode)}
|
||||
src={getBendingImageUrl('bottombar', bendingInfo.productCode, undefined, bendingImages)}
|
||||
alt="하단마감재"
|
||||
className="w-full border border-gray-300"
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,8 @@ interface GuideRailSectionProps {
|
||||
bendingInfo: BendingInfoExtended;
|
||||
mapping: MaterialMapping;
|
||||
lotNo: string;
|
||||
lotNoMap?: Record<string, string>; // BD-{prefix}-{lengthCode} → LOT NO
|
||||
lotNoMap?: Record<string, string>;
|
||||
bendingImages?: Record<string, string>;
|
||||
}
|
||||
|
||||
function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }: {
|
||||
@@ -76,7 +77,7 @@ function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }:
|
||||
);
|
||||
}
|
||||
|
||||
export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap }: GuideRailSectionProps) {
|
||||
export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap, bendingImages }: GuideRailSectionProps) {
|
||||
const { wall, side } = bendingInfo.guideRail;
|
||||
const productCode = bendingInfo.productCode;
|
||||
|
||||
@@ -100,7 +101,7 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap }: Guid
|
||||
<PartTable
|
||||
title="1.1 벽면형 [130*75]"
|
||||
rows={wallRows}
|
||||
imageUrl={getBendingImageUrl('guiderail', productCode, 'wall')}
|
||||
imageUrl={getBendingImageUrl('guiderail', productCode, 'wall', bendingImages)}
|
||||
lotNo={lotNo}
|
||||
baseSize={wall?.baseDimension || wall?.baseSize}
|
||||
lotNoMap={lotNoMap}
|
||||
@@ -111,7 +112,7 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap }: Guid
|
||||
<PartTable
|
||||
title="1.2 측면형 [130*125]"
|
||||
rows={sideRows}
|
||||
imageUrl={getBendingImageUrl('guiderail', productCode, 'side')}
|
||||
imageUrl={getBendingImageUrl('guiderail', productCode, 'side', bendingImages)}
|
||||
lotNo={lotNo}
|
||||
baseSize={side?.baseDimension || '135*130'}
|
||||
lotNoMap={lotNoMap}
|
||||
|
||||
@@ -13,9 +13,10 @@ import { buildShutterBoxRows, getBendingImageUrl, fmt, fmtWeight, lookupLotNo }
|
||||
interface ShutterBoxSectionProps {
|
||||
bendingInfo: BendingInfoExtended;
|
||||
lotNoMap?: Record<string, string>;
|
||||
bendingImages?: Record<string, string>;
|
||||
}
|
||||
|
||||
function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; index: number; lotNoMap?: Record<string, string> }) {
|
||||
function ShutterBoxSubSection({ box, index, lotNoMap, bendingImages }: { box: ShutterBoxData; index: number; lotNoMap?: Record<string, string>; bendingImages?: Record<string, string> }) {
|
||||
const rows = buildShutterBoxRows(box);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
@@ -37,7 +38,7 @@ function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; i
|
||||
<div className="flex-shrink-0 w-48">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={getBendingImageUrl('box', '', imageType)}
|
||||
src={getBendingImageUrl('box', '', imageType, bendingImages)}
|
||||
alt={`셔터박스 ${box.direction}`}
|
||||
className="w-full border border-gray-300"
|
||||
/>
|
||||
@@ -92,7 +93,7 @@ function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; i
|
||||
);
|
||||
}
|
||||
|
||||
export function ShutterBoxSection({ bendingInfo, lotNoMap }: ShutterBoxSectionProps) {
|
||||
export function ShutterBoxSection({ bendingInfo, lotNoMap, bendingImages }: ShutterBoxSectionProps) {
|
||||
const boxes = bendingInfo.shutterBox;
|
||||
if (!boxes || boxes.length === 0) return null;
|
||||
|
||||
@@ -103,7 +104,7 @@ export function ShutterBoxSection({ bendingInfo, lotNoMap }: ShutterBoxSectionPr
|
||||
</div>
|
||||
|
||||
{boxes.map((box, idx) => (
|
||||
<ShutterBoxSubSection key={idx} box={box} index={idx} lotNoMap={lotNoMap} />
|
||||
<ShutterBoxSubSection key={idx} box={box} index={idx} lotNoMap={lotNoMap} bendingImages={bendingImages} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,10 @@ import { buildSmokeBarrierRows, getBendingImageUrl, fmt, fmtWeight, lookupLotNo
|
||||
interface SmokeBarrierSectionProps {
|
||||
bendingInfo: BendingInfoExtended;
|
||||
lotNoMap?: Record<string, string>;
|
||||
bendingImages?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function SmokeBarrierSection({ bendingInfo, lotNoMap }: SmokeBarrierSectionProps) {
|
||||
export function SmokeBarrierSection({ bendingInfo, lotNoMap, bendingImages }: SmokeBarrierSectionProps) {
|
||||
const rows = buildSmokeBarrierRows(bendingInfo.smokeBarrier);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
@@ -30,7 +31,7 @@ export function SmokeBarrierSection({ bendingInfo, lotNoMap }: SmokeBarrierSecti
|
||||
{/* 좌측: 이미지 */}
|
||||
<div className="flex-shrink-0 w-48">
|
||||
<img
|
||||
src={getBendingImageUrl('smokebarrier', '')}
|
||||
src={getBendingImageUrl('smokebarrier', '', undefined, bendingImages)}
|
||||
alt="연기차단재"
|
||||
className="w-full border border-gray-300"
|
||||
/>
|
||||
|
||||
@@ -127,11 +127,42 @@ export function getMaterialMapping(productCode: string, finishMaterial: string):
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.sam.kr';
|
||||
|
||||
export function getBendingImageUrl(
|
||||
/**
|
||||
* 절곡 이미지 키 생성 (bending_images 맵 조회용)
|
||||
*/
|
||||
export function getBendingImageKey(
|
||||
category: 'guiderail' | 'bottombar' | 'smokebarrier' | 'box',
|
||||
productCode: string,
|
||||
type?: 'wall' | 'side' | 'both' | 'bottom' | 'rear',
|
||||
): string {
|
||||
switch (category) {
|
||||
case 'guiderail': {
|
||||
const isLargeProfile = ['KQTS01', 'KTE01'].includes(productCode);
|
||||
const size = isLargeProfile
|
||||
? (type === 'wall' ? '130x75' : '130x125')
|
||||
: (type === 'wall' ? '120x70' : '120x120');
|
||||
return `guiderail_${productCode}_${type}_${size}`;
|
||||
}
|
||||
case 'bottombar':
|
||||
return `bottombar_${productCode}`;
|
||||
case 'smokebarrier':
|
||||
return 'smokeban';
|
||||
case 'box':
|
||||
return `box_${type || 'both'}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getBendingImageUrl(
|
||||
category: 'guiderail' | 'bottombar' | 'smokebarrier' | 'box',
|
||||
productCode: string,
|
||||
type?: 'wall' | 'side' | 'both' | 'bottom' | 'rear',
|
||||
bendingImages?: Record<string, string>,
|
||||
): string {
|
||||
const key = getBendingImageKey(category, productCode, type);
|
||||
if (bendingImages?.[key]) return bendingImages[key];
|
||||
// fallback: API 서버 직접 (레거시)
|
||||
switch (category) {
|
||||
case 'guiderail': {
|
||||
const isLargeProfile = ['KQTS01', 'KTE01'].includes(productCode);
|
||||
|
||||
@@ -63,6 +63,7 @@ export function WorkLogModal({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [bendingImages, setBendingImages] = useState<Record<string, string>>({});
|
||||
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
||||
// Lazy Snapshot 대상 문서 ID
|
||||
const [snapshotDocumentId, setSnapshotDocumentId] = useState<number | null>(null);
|
||||
@@ -129,6 +130,10 @@ export function WorkLogModal({
|
||||
if (lotsResult.success) {
|
||||
setMaterialLots(lotsResult.data);
|
||||
}
|
||||
// bending_images 맵 저장
|
||||
if (workLogResult.success && workLogResult.data?.bending_images) {
|
||||
setBendingImages(workLogResult.data.bending_images);
|
||||
}
|
||||
// Lazy Snapshot: 문서가 있고 rendered_html이 없으면 스냅샷 대상
|
||||
if (workLogResult.success && workLogResult.data?.document) {
|
||||
const doc = workLogResult.data.document as { id?: number; rendered_html?: string | null };
|
||||
@@ -147,6 +152,7 @@ export function WorkLogModal({
|
||||
// 모달 닫힐 때 상태 초기화
|
||||
setOrder(null);
|
||||
setMaterialLots([]);
|
||||
setBendingImages({});
|
||||
setSnapshotDocumentId(null);
|
||||
setError(null);
|
||||
}
|
||||
@@ -250,7 +256,7 @@ export function WorkLogModal({
|
||||
lotNoMap[lot.item_code] = lot.lot_no;
|
||||
}
|
||||
}
|
||||
return <BendingWorkLogContent data={order} lotNoMap={lotNoMap} />;
|
||||
return <BendingWorkLogContent data={order} lotNoMap={lotNoMap} bendingImages={bendingImages} />;
|
||||
}
|
||||
default:
|
||||
return <WorkLogContent data={order} />;
|
||||
|
||||
@@ -771,6 +771,7 @@ export async function getWorkLog(
|
||||
document: Record<string, unknown> | null;
|
||||
auto_values: Record<string, string>;
|
||||
work_stats: Record<string, unknown>;
|
||||
bending_images: Record<string, string>;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
@@ -779,6 +780,7 @@ export async function getWorkLog(
|
||||
document: Record<string, unknown> | null;
|
||||
auto_values: Record<string, string>;
|
||||
work_stats: Record<string, unknown>;
|
||||
bending_images: Record<string, string>;
|
||||
}>({
|
||||
url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`,
|
||||
errorMessage: '작업일지 조회에 실패했습니다.',
|
||||
|
||||
Reference in New Issue
Block a user