diff --git a/.claude/hooks/block-build.sh b/.claude/hooks/block-build.sh new file mode 100755 index 00000000..7f42a8ed --- /dev/null +++ b/.claude/hooks/block-build.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# PreToolUse Hook: 빌드 명령 차단 +# CLAUDE.md 규칙: "Claude가 직접 npm run build 실행 금지" + +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# 빌드 명령 패턴 체크 +if echo "$COMMAND" | grep -qE '(npm run build|next build|yarn build|pnpm build)(\s|$|;|&&|\|)'; then + echo "🚫 빌드 명령이 차단되었습니다." >&2 + echo " CLAUDE.md 규칙: Claude가 직접 빌드 실행 금지" >&2 + echo " 빌드가 필요하면 사용자에게 요청하세요." >&2 + exit 2 +fi + +exit 0 diff --git a/.claude/hooks/check-file-size.sh b/.claude/hooks/check-file-size.sh new file mode 100755 index 00000000..1977423e --- /dev/null +++ b/.claude/hooks/check-file-size.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# PreToolUse Hook: Write 전 파일 크기 급감 경고 +# 기존 파일 대비 50% 이상 줄어들면 경고 (최소 50줄 이상 파일만) + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') +NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty') + +# 파일이 없으면 (신규 생성) 통과 +if [ ! -f "$FILE_PATH" ]; then + exit 0 +fi + +EXISTING_LINES=$(wc -l < "$FILE_PATH" 2>/dev/null | tr -d ' ') +NEW_LINES=$(echo "$NEW_CONTENT" | wc -l | tr -d ' ') + +# 기존 파일이 50줄 미만이면 체크 스킵 +if [ "$EXISTING_LINES" -lt 50 ] 2>/dev/null; then + exit 0 +fi + +# 새 내용이 기존의 50% 미만이면 경고 +THRESHOLD=$((EXISTING_LINES / 2)) +if [ "$NEW_LINES" -lt "$THRESHOLD" ] 2>/dev/null; then + echo "⚠️ File size drop: $FILE_PATH" >&2 + echo " 기존: ${EXISTING_LINES}줄 → 새 내용: ${NEW_LINES}줄 (${THRESHOLD}줄 미만)" >&2 + echo " 파일 내용이 절반 이상 줄었습니다. 의도한 변경인지 확인하세요." >&2 + exit 2 +fi + +exit 0 diff --git a/.claude/hooks/check-unused-imports.sh b/.claude/hooks/check-unused-imports.sh new file mode 100755 index 00000000..35a689db --- /dev/null +++ b/.claude/hooks/check-unused-imports.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# PostToolUse Hook: Edit/Write 후 미사용 import 체크 +# 단일 파일 eslint로 빠르게 체크 (전체 tsc보다 빠름) + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +# TypeScript 파일만 체크 +if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx ]]; then + exit 0 +fi + +# 파일 존재 확인 +if [ ! -f "$FILE_PATH" ]; then + exit 0 +fi + +cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0 + +# eslint로 미사용 변수/import 체크 (단일 파일 → 빠름) +RESULT=$(npx eslint --no-eslintrc --rule '{"@typescript-eslint/no-unused-vars": "error"}' --parser @typescript-eslint/parser --plugin @typescript-eslint "$FILE_PATH" 2>&1 | grep "no-unused-vars" | head -10) + +if [ -n "$RESULT" ]; then + echo "Unused imports/variables in $FILE_PATH:" >&2 + echo "$RESULT" >&2 + exit 2 +fi + +exit 0 diff --git a/claudedocs/[PLAN-2026-02-04] quality-audit-document-management.md b/claudedocs/[PLAN-2026-02-04] quality-audit-document-management.md new file mode 100644 index 00000000..bb8194bf --- /dev/null +++ b/claudedocs/[PLAN-2026-02-04] quality-audit-document-management.md @@ -0,0 +1,27 @@ +# [PLAN] 품질인정심사 - 문서 관리 시스템 + +## 개요 +품질인정심사 시스템의 점검 항목별 관련 문서(PDF, Excel)를 업로드하고 맵핑하는 기능 + +## 필요 페이지 + +### 1. 관리자 페이지 (신규) +- 점검 항목별 관련 문서 업로드 (PDF, Excel) +- 문서 ↔ 점검항목 맵핑 관리 +- 버전 관리 (REV번호, 등록일) +- 파일 교체/삭제 + +### 2. 품질인정심사 페이지 (기존 화면 수정) +- 맵핑된 문서 목록 표시 (현재 UI에 이미 있음) +- 보기: PDF → 새 탭에서 브라우저 표시 / Excel → 다운로드 후 실행 +- 다운로드: 파일 직접 다운로드 +- 오른쪽 미리보기 영역: 파일 정보(파일명, 크기, 등록일, 등록자) 표시 + +## 백엔드 API 필요사항 +- 파일 업로드 API +- 문서 ↔ 점검항목 맵핑 CRUD API +- 파일 다운로드/스트리밍 API + +## 상태 +- 기획 대기 중 +- 기획서 나오면 상세 설계 진행 예정 diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx index 87e117b1..5bc2c8fa 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx @@ -3,12 +3,15 @@ /** * 조인트바 중간검사 성적서 문서 컴포넌트 * - * 공통 컴포넌트 사용: - * - DocumentHeader: quality 레이아웃 + customApproval (QualityApprovalTable) + * 새 기획서(SlatJointBarInspectionContent) 레이아웃 적용: + * - 헤더: 다른 중간검사 성적서(스크린/절곡/슬랫)와 동일한 헤딩 + 결재 테이블 + * - 기본정보: 제품명/슬랫, 규격/슬랫, 수주처, 부서 + * - 중간검사 기준서 KOPS-20: 도해 이미지 + 겉모양(3행) + 치수(4행) + * - 중간검사 DATA: No, 겉모양(가공/조립), ①②③④ 치수, 판정 + * - 부적합 내용 + 종합판정 (한 행) */ import React from 'react'; -import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 조인트바 중간검사 성적서 데이터 타입 export interface JointbarInspectionData { @@ -18,7 +21,7 @@ export interface JointbarInspectionData { client: string; siteName: string; lotNo: string; - lotSize: string; + department: string; inspectionDate: string; inspector: string; approvers: { @@ -26,23 +29,16 @@ export interface JointbarInspectionData { reviewer?: string; approver?: string; }; - // 중간검사 기준서 정보 - standardInfo: { - appearance: { criteria: string; method: string; frequency: string; regulation: string }; - assembly: { criteria: string; method: string; frequency: string; regulation: string }; - coating: { criteria: string; method: string; frequency: string; regulation: string }; - dimensions: { criteria: string; method: string; frequency: string; regulation: string }; - }; // 중간검사 DATA inspectionData: { serialNo: string; processState: '양호' | '불량'; assemblyState: '양호' | '불량'; - height: { standard: number; measured: number }; - height2: { standard: number; measured: number }; - bandLength: { standard: number; measured: number }; - gap: { standard: number; measured: number }; - result: '적합' | '부적합'; + height1: { standard: string; measured: string }; + height2: { standard: string; measured: string }; + length: { standard: string; measured: string }; + interval: { standard: string; measured: string }; + result: '적' | '부'; }[]; notes: string; overallResult: '합격' | '불합격'; @@ -51,12 +47,12 @@ export interface JointbarInspectionData { // Mock 데이터 export const MOCK_JOINTBAR_INSPECTION: JointbarInspectionData = { documentNo: 'KDQP-01-009', - productName: '조인트바', - specification: '와이어 클러치 크립지름', + productName: '슬랫', + specification: '슬랫', client: '주일', siteName: '용산고등학교(4호)', lotNo: 'KD-WE-251015-01-(3)', - lotSize: '11 개소', + department: '생산부', inspectionDate: '2025.', inspector: '', approvers: { @@ -64,38 +60,12 @@ export const MOCK_JOINTBAR_INSPECTION: JointbarInspectionData = { reviewer: '', approver: '', }, - standardInfo: { - appearance: { - criteria: '사용상 해로운 결함이 없을 것', - method: '', - frequency: 'n = 1, c = 0', - regulation: 'KS F 4510 5.1항', - }, - assembly: { - criteria: '밴드시트 읍동에 의해\n견고하게 조립되어야 함', - method: '확인점검', - frequency: '', - regulation: 'KS F 4510 9항', - }, - coating: { - criteria: '용접부위에 락터스베이\n도포하여야 함', - method: '', - frequency: '', - regulation: '자체규정', - }, - dimensions: { - criteria: '⓪\n16.5 ± 1\n14.5 ± 1\n300(밴드마감재) ± 4\n150 ± 4', - method: '체크검사', - frequency: '', - regulation: 'KS F 4510 7항\n외 9', - }, - }, inspectionData: [ - { serialNo: '1', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' }, - { serialNo: '2', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' }, - { serialNo: '3', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' }, - { serialNo: '4', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' }, - { serialNo: '5', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' }, + { serialNo: '1', processState: '양호', assemblyState: '양호', height1: { standard: '43.1 ± 0.5', measured: '43.1' }, height2: { standard: '14.5 ± 1', measured: '14.5' }, length: { standard: '', measured: '' }, interval: { standard: '150 ± 4', measured: '150' }, result: '적' }, + { serialNo: '2', processState: '양호', assemblyState: '양호', height1: { standard: '43.1 ± 0.5', measured: '43.1' }, height2: { standard: '14.5 ± 1', measured: '14.5' }, length: { standard: '', measured: '' }, interval: { standard: '150 ± 4', measured: '150' }, result: '적' }, + { serialNo: '3', processState: '양호', assemblyState: '양호', height1: { standard: '43.1 ± 0.5', measured: '43.1' }, height2: { standard: '14.5 ± 1', measured: '14.5' }, length: { standard: '', measured: '' }, interval: { standard: '150 ± 4', measured: '150' }, result: '적' }, + { serialNo: '4', processState: '양호', assemblyState: '양호', height1: { standard: '43.1 ± 0.5', measured: '43.1' }, height2: { standard: '14.5 ± 1', measured: '14.5' }, length: { standard: '', measured: '' }, interval: { standard: '150 ± 4', measured: '150' }, result: '적' }, + { serialNo: '5', processState: '양호', assemblyState: '양호', height1: { standard: '43.1 ± 0.5', measured: '43.1' }, height2: { standard: '14.5 ± 1', measured: '14.5' }, length: { standard: '', measured: '' }, interval: { standard: '150 ± 4', measured: '150' }, result: '적' }, ], notes: '', overallResult: '합격', @@ -108,37 +78,57 @@ interface JointbarInspectionDocumentProps { export const JointbarInspectionDocument = ({ data = MOCK_JOINTBAR_INSPECTION }: JointbarInspectionDocumentProps) => { return (
- {/* 문서 헤더 (공통 컴포넌트) */} - - } - /> + {/* 헤더 영역 - 다른 중간검사 성적서와 동일 패턴 */} +
+
+

중간검사성적서 (조인트바)

+

+ 문서번호: {data.documentNo} | 검사일자: {data.inspectionDate} +

+
+ {/* 결재란 */} + + + + + + + + + + + + + + + + + + + + + + +

작성승인승인승인
{data.approvers.writer || ''}{data.approvers.reviewer || '이름'}{data.approvers.approver || '이름'}이름
부서명부서명부서명부서명
+
{/* 기본 정보 테이블 */} - + - + - - + + - + @@ -152,131 +142,174 @@ export const JointbarInspectionDocument = ({ data = MOCK_JOINTBAR_INSPECTION }:
품 명제품명 {data.productName}제품 LOT NO제품 LOT NO {data.lotNo}
규 격 {data.specification}로트크기{data.lotSize}부서{data.department}
발주처수주처 {data.client} 검사일자 {data.inspectionDate}
- {/* 중간검사 기준서 */} - - - - - - - - - - - - + {/* 중간검사 기준서 KOPS-20 */} +
■ 중간검사 기준서 KOPS-20
+
중간검사
기준서
도해검사항목검사기준검사방법검사주기관련규정
- - - - - - + {/* 헤더 행 */} + + + + + + + {/* 겉모양 > 가공상태 */} + + + + + + + + + {/* 겉모양 > 조립상태 */} + + + + - - - - - + + + {/* 치수 > ① 높이 */} - - - - - + + + + + + {/* 치수 > ② 높이 */} - - - - - + + + + {/* 치수 > 길이 */} + + + + + {/* 치수 > 간격 */} + + + +
- {/* 도해 이미지 영역 */} -
-
-
- 1 -
-
- 조인 -
-
- 2 -
-
-
+ {/* 도해 영역 */} +
+
도해 이미지 영역
가공상태{data.standardInfo.appearance.criteria}{data.standardInfo.appearance.frequency}{data.standardInfo.appearance.regulation}검사항목검사기준검사방법검사주기관련규정
겉모양가공상태사용상 해로운 결함이 없을것육안검사n = 1, c = 0KS F 4510 5.1항
조립상태엔드락이 용접에 의해
견고하게 조립되어야 함
KS F 4510 9항
{data.standardInfo.assembly.criteria}{data.standardInfo.assembly.method}{data.standardInfo.assembly.regulation}용접부위에 락카도색이
되어야 함
자체규정
{data.standardInfo.coating.criteria}{data.standardInfo.coating.regulation}치수
(mm)
① 높이43.1 ± 0.5체크검사KS F 4510 7항
표9
치수
(mm)
{data.standardInfo.dimensions.criteria}{data.standardInfo.dimensions.method}{data.standardInfo.dimensions.regulation}② 높이14.5 ± 1
길이도면치수 ± 4
간격150 ± 4자체규정
{/* 중간검사 DATA */} -
중간검사 DATA
+
■ 중간검사 DATA
- - - - + + + + + + + - - - - - - - - - - + + + + + + + + + + {data.inspectionData.map((item, index) => ( - - + {/* 가공상태 */} + - - - - - - - - - - + + {/* ② 높이 */} + + + {/* ③ 길이 */} + + + {/* ④ 간격 */} + + + {/* 판정 */} + ))}
일련
번호
검사항치수 [mm]판정No.겉모양① 높이 (mm)② 높이 (mm)③ 길이 (mm)④ 간격판정
(적/부)
가공상태조립상태⓪ 높이
기준치
측정값⓪ 높이
기준치
측정값⓪ 길이 (밴드마감재)
기준치
측정값⓪ 간격
기준치
측정값가공상태조립상태기준치측정값기준치측정값기준치측정값기준치측정값
{item.serialNo} - ☐ 양호 ☐ 불량 + {item.serialNo} +
+ + + {item.processState === '양호' ? '✓' : ''} + + 양호 + + + + {item.processState === '불량' ? '✓' : ''} + + 불량 + +
- ☐ 양호 ☐ 불량 + {/* 조립상태 */} + +
+ + + {item.assemblyState === '양호' ? '✓' : ''} + + 양호 + + + + {item.assemblyState === '불량' ? '✓' : ''} + + 불량 + +
{item.height.standard} ± 1{item.height.measured}{item.height2.standard} ± 1{item.height2.measured}{item.bandLength.standard} ± 4{item.bandLength.measured}{item.gap.standard} ± 4{item.gap.measured} - ☐ 적합 ☐ 부
적합 + {/* ① 높이 */} +
{item.height1.standard}{item.height1.measured || '-'}{item.height2.standard}{item.height2.measured || '-'}{item.length.standard || '-'}{item.length.measured || '-'}{item.interval.standard}{item.interval.measured || '-'} + {item.result}
- {/* 부적합 내용 */} -
-
【부적합 내용】
-
{data.notes}
-
+ {/* 부적합 내용 + 종합판정 */} + + + + + + + + + +
부적합 내용{data.notes || '\u00A0'}종합판정 + {data.overallResult} +
- {/* 문서번호 및 종합판정 */} + {/* 문서번호 */}
{data.documentNo}
-
-
종합판정
-
- {data.overallResult} -
-
KDPS-10-03
diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index 1724d3f2..ad299191 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/table"; import { Edit, + PenLine, Factory, XCircle, FileSpreadsheet, @@ -32,6 +33,7 @@ import { Eye, CheckCircle2, RotateCcw, + Undo2, Package, ChevronDown, ChevronRight, @@ -776,55 +778,55 @@ export default function OrderDetailPage() { const showRevertConfirmButton = order.status === "order_confirmed"; return ( -
+ <> {/* 견적 수정 */} {showEditQuoteButton && ( - )} {/* 수주서 보기 */} - {/* 수주 확정 */} {showConfirmButton && ( - )} {/* 수주확정 되돌리기 */} {showRevertConfirmButton && ( - )} {/* 생산지시 생성 */} {showProductionCreateButton && ( - )} {/* 생산지시 되돌리기 */} {showRevertButton && ( - )} {/* 수정 */} {showEditButton && ( - )} -
+ ); }, [order, handleEditQuote, handleViewOrderDocument, handleConfirmOrder, handleRevertConfirmation, handleProductionOrder, handleRevertProduction, handleEdit]); diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx index 7f38d918..be815ae1 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx @@ -967,18 +967,19 @@ export default function ProductionOrderCreatePage() { PRIORITY_COLORS[selectedPriority].buttonActive, "border-0" )}> - 우선순위: {selectedConfig.productionOrder} + 우선순위: {selectedConfig.productionOrder} )} -
- -
diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index c5ec7f86..abb2badd 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -27,7 +27,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { DeleteConfirmDialog, SaveConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { badDebtConfig } from './badDebtConfig'; import { toast } from 'sonner'; @@ -43,7 +42,6 @@ import { } from './types'; import { createBadDebt, updateBadDebt, deleteBadDebt, addBadDebtMemo, deleteBadDebtMemo } from './actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { usePermission } from '@/hooks/usePermission'; interface BadDebtDetailProps { mode: 'view' | 'edit' | 'new'; @@ -96,7 +94,6 @@ const getEmptyRecord = (): Omit export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProps) { const router = useRouter(); - const { canUpdate, canDelete } = usePermission(); const isViewMode = mode === 'view'; const isNewMode = mode === 'new'; @@ -114,9 +111,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp }, }); - // 다이얼로그 상태 - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [showSaveDialog, setShowSaveDialog] = useState(false); + // 상태 const [isLoading, setIsLoading] = useState(false); // 새 메모 입력 @@ -132,82 +127,43 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp setFormData(prev => ({ ...prev, [field]: value })); }, []); - // 네비게이션 핸들러 - const handleEdit = useCallback(() => { - router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=edit`); - }, [router, recordId]); - - const handleCancel = useCallback(() => { - if (isNewMode) { - router.push('/ko/accounting/bad-debt-collection'); - } else { - router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`); - } - }, [router, recordId, isNewMode]); - - // 저장 핸들러 - const handleSave = useCallback(() => { - setShowSaveDialog(true); - }, []); - - const handleConfirmSave = useCallback(async () => { - setIsLoading(true); - setShowSaveDialog(false); - + // 저장/등록 핸들러 (IntegratedDetailTemplate onSubmit용) + const handleTemplateSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { try { if (isNewMode) { const result = await createBadDebt(formData); if (result.success) { - toast.success('악성채권이 등록되었습니다.'); - router.push('/ko/accounting/bad-debt-collection'); - } else { - toast.error(result.error || '등록에 실패했습니다.'); + return { success: true }; } + return { success: false, error: result.error || '등록에 실패했습니다.' }; } else { const result = await updateBadDebt(recordId!, formData); if (result.success) { - toast.success('악성채권이 수정되었습니다.'); - router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`); - } else { - toast.error(result.error || '수정에 실패했습니다.'); + return { success: true }; } + return { success: false, error: result.error || '수정에 실패했습니다.' }; } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('저장 오류:', error); - toast.error('서버 오류가 발생했습니다.'); - } finally { - setIsLoading(false); + return { success: false, error: '서버 오류가 발생했습니다.' }; } - }, [formData, router, recordId, isNewMode]); - - // 삭제 핸들러 - const handleDelete = useCallback(() => { - setShowDeleteDialog(true); - }, []); - - const handleConfirmDelete = useCallback(async () => { - if (!recordId) return; - - setIsLoading(true); - setShowDeleteDialog(false); + }, [formData, recordId, isNewMode]); + // 삭제 핸들러 (IntegratedDetailTemplate onDelete용) + const handleTemplateDelete = useCallback(async (id: string | number): Promise<{ success: boolean; error?: string }> => { try { - const result = await deleteBadDebt(recordId); + const result = await deleteBadDebt(String(id)); if (result.success) { - toast.success('악성채권이 삭제되었습니다.'); - router.push('/ko/accounting/bad-debt-collection'); - } else { - toast.error(result.error || '삭제에 실패했습니다.'); + return { success: true }; } + return { success: false, error: result.error || '삭제에 실패했습니다.' }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('삭제 오류:', error); - toast.error('서버 오류가 발생했습니다.'); - } finally { - setIsLoading(false); + return { success: false, error: '서버 오류가 발생했습니다.' }; } - }, [router, recordId]); + }, []); // 메모 추가 핸들러 const handleAddMemo = useCallback(async () => { @@ -340,39 +296,16 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp return { ...badDebtConfig, title: titleMap[mode] || badDebtConfig.title, + actions: { + ...badDebtConfig.actions, + deleteConfirmMessage: { + title: '악성채권 삭제', + description: '이 악성채권 기록을 삭제하시겠습니까? 확인 클릭 시 목록으로 이동합니다.', + }, + }, }; }, [mode]); - // 커스텀 헤더 액션 (저장 확인 다이얼로그 패턴 유지) - const customHeaderActions = useMemo(() => { - if (isViewMode) { - return ( - <> - {canDelete && ( - - )} - {canUpdate && ( - - )} - - ); - } - return ( - <> - - - - ); - }, [isViewMode, isNewMode, isLoading, handleDelete, handleEdit, handleCancel, handleSave, mode, canUpdate, canDelete]); - // 입력 필드 렌더링 헬퍼 const renderField = ( label: string, @@ -1019,40 +952,16 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp ]); return ( - <> - } - itemId={recordId} - isLoading={isLoading} - headerActions={customHeaderActions} - renderView={() => renderFormContent()} - renderForm={() => renderFormContent()} - /> - - {/* 삭제 확인 다이얼로그 */} - - '{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까? -
- 확인 클릭 시 목록으로 이동합니다. - - } - /> - - {/* 저장 확인 다이얼로그 */} - - + } + itemId={recordId} + isLoading={isLoading} + onSubmit={handleTemplateSubmit} + onDelete={handleTemplateDelete} + renderView={() => renderFormContent()} + renderForm={() => renderFormContent()} + /> ); } \ No newline at end of file diff --git a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx index 59bc1128..2f160951 100644 --- a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx +++ b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx @@ -145,8 +145,8 @@ export function VendorLedgerDetail({ size="sm" onClick={handlePdfDownload} > - - PDF 다운로드 + + PDF 다운로드 ); }, [handlePdfDownload]); diff --git a/src/components/board/BoardDetail/index.tsx b/src/components/board/BoardDetail/index.tsx index f0306369..f60d1a0d 100644 --- a/src/components/board/BoardDetail/index.tsx +++ b/src/components/board/BoardDetail/index.tsx @@ -201,24 +201,25 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }: {/* 하단 액션 버튼 (sticky) */} -
- {isMyPost && ( -
+
-
)} diff --git a/src/components/business/construction/contract/ContractDetailForm.tsx b/src/components/business/construction/contract/ContractDetailForm.tsx index 984c6045..60e86866 100644 --- a/src/components/business/construction/contract/ContractDetailForm.tsx +++ b/src/components/business/construction/contract/ContractDetailForm.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { Eye, Download, FileText, X } from 'lucide-react'; +import { Eye, Download, FileText, X, FilePlus2, Stamp } from 'lucide-react'; import { FileInput } from '@/components/ui/file-input'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list'; @@ -232,15 +232,17 @@ export default function ContractDetailForm({ if (!isViewMode) return null; return ( <> - - - ); diff --git a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx index d16574ad..740e08e1 100644 --- a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx +++ b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { Plus, X, Eye } from 'lucide-react'; +import { Plus, X, Eye, Stamp } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -149,12 +149,13 @@ export default function HandoverReportDetailForm({ if (!isViewMode) return null; return ( <> - - ); diff --git a/src/components/business/construction/labor-management/LaborDetailClient.tsx b/src/components/business/construction/labor-management/LaborDetailClient.tsx index b7d01e29..870f1a90 100644 --- a/src/components/business/construction/labor-management/LaborDetailClient.tsx +++ b/src/components/business/construction/labor-management/LaborDetailClient.tsx @@ -404,53 +404,54 @@ export default function LaborDetailClient({
{/* 하단 액션 버튼 (sticky) */} -
- -
+
{mode === 'view' && ( <> {canDelete && ( )} {canUpdate && ( - )} )} {mode === 'edit' && ( <> - - )} {mode === 'new' && ( <> - - )} diff --git a/src/components/business/construction/pricing-management/PricingDetailClient.tsx b/src/components/business/construction/pricing-management/PricingDetailClient.tsx index 3ac49820..3a73c0cb 100644 --- a/src/components/business/construction/pricing-management/PricingDetailClient.tsx +++ b/src/components/business/construction/pricing-management/PricingDetailClient.tsx @@ -397,53 +397,54 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
{/* 하단 액션 버튼 (sticky) */} -
- -
+
{isViewMode && ( <> {canDelete && ( )} {canUpdate && ( - )} )} {isEditMode && ( <> - - )} {isCreateMode && ( <> - - )} diff --git a/src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx b/src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx index e35dc6b7..f474601e 100644 --- a/src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx +++ b/src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { Eye } from 'lucide-react'; +import { Hammer, Wrench, Image } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { progressBillingConfig } from './progressBillingConfig'; @@ -91,17 +91,17 @@ export default function ProgressBillingDetailForm({ if (!isViewMode) return null; return ( <> - - - ); diff --git a/src/components/clients/ClientDetail.tsx b/src/components/clients/ClientDetail.tsx index 861c207f..8cee2ee0 100644 --- a/src/components/clients/ClientDetail.tsx +++ b/src/components/clients/ClientDetail.tsx @@ -233,19 +233,19 @@ export function ClientDetail({
{/* 하단 액션 버튼 (sticky) */} -
- -
- -
diff --git a/src/components/items/DynamicItemForm/index.tsx b/src/components/items/DynamicItemForm/index.tsx index 30a54ddd..c6a7ce5f 100644 --- a/src/components/items/DynamicItemForm/index.tsx +++ b/src/components/items/DynamicItemForm/index.tsx @@ -1046,22 +1046,26 @@ export default function DynamicItemForm({ /> {/* 하단 액션 버튼 (sticky) */} -
+
diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx index 82b36b87..1b3a20df 100644 --- a/src/components/items/ItemDetailClient.tsx +++ b/src/components/items/ItemDetailClient.tsx @@ -618,22 +618,26 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) { )} {/* 하단 액션 버튼 (sticky) */} -
+
{canUpdate && ( )}
diff --git a/src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx b/src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx index 52ff3d57..81ea0e4d 100644 --- a/src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx +++ b/src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx @@ -84,11 +84,11 @@ export function SectionsTab({ return ( -
-
-
+
+
+
섹션 템플릿 관리 - 재사용 가능한 섹션 템플릿을 관리합니다 + 재사용 가능한 섹션 템플릿을 관리합니다
{/* 변경사항 배지 - 나중에 사용 예정으로 임시 숨김 */} {false && hasUnsavedChanges && pendingChanges.sectionTemplates.length > 0 && ( @@ -97,8 +97,8 @@ export function SectionsTab({ )}
-
@@ -143,19 +143,19 @@ export function SectionsTab({ {sectionTemplates.filter(t => t.section_type !== 'BOM').map((template) => ( -
-
- -
- {template.template_name} +
+
+ +
+ {template.template_name} {template.description && ( - {template.description} + {template.description} )}
-
+
{template.category && template.category.length > 0 && ( -
+
{template.category.map((cat, idx) => ( {ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat} @@ -193,11 +193,11 @@ export function SectionsTab({
-
+

이 템플릿과 관련되는 항목 목록을 조회합니다

-
+
{setIsImportFieldDialogOpen && setImportFieldTargetSectionId && ( - {isViewMode && ( - - )} -
+ ) : undefined; // 에러 상태 표시 (view/edit 모드에서만) diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index e5e57c2b..d48f6304 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -254,21 +254,21 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { return ( <> {showConfirmButton && ( - )} {showProductionCreateButton && ( - )} {showCancelButton && ( - )} diff --git a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx index 4e01a33e..50f289af 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx @@ -272,56 +272,45 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { if (!detail) return null; return ( -
+ <> - {/* 거래명세서 - 추후 활성화 */} - {/* */} {canDelete && ( - <> -
- - + )} {STATUS_TRANSITIONS[detail.status] && ( - <> -
- - + )} -
+ ); }, [detail, canDelete, handleOpenStatusDialog]); diff --git a/src/components/pricing-table-management/PricingTableForm.tsx b/src/components/pricing-table-management/PricingTableForm.tsx index 850f920b..baa5ce29 100644 --- a/src/components/pricing-table-management/PricingTableForm.tsx +++ b/src/components/pricing-table-management/PricingTableForm.tsx @@ -437,33 +437,33 @@ export function PricingTableForm({ mode, initialData }: PricingTableFormProps) { {/* 하단 액션 버튼 (sticky) */}
- -
+
{isEdit ? ( <> {canDelete && ( - )} {canUpdate && ( - )} ) : ( canCreate && ( - ) )} diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx index 8e2685ba..71a9f40f 100644 --- a/src/components/process-management/ProcessDetail.tsx +++ b/src/components/process-management/ProcessDetail.tsx @@ -321,16 +321,16 @@ export function ProcessDetail({ process }: ProcessDetailProps) { {/* 하단 액션 버튼 (sticky) */}
- {canUpdate && ( - )}
diff --git a/src/components/process-management/StepDetail.tsx b/src/components/process-management/StepDetail.tsx index 2ae419cb..ed62e9a1 100644 --- a/src/components/process-management/StepDetail.tsx +++ b/src/components/process-management/StepDetail.tsx @@ -125,16 +125,16 @@ export function StepDetail({ step, processId }: StepDetailProps) { {/* 하단 액션 버튼 (sticky) */}
- {canUpdate && ( - )}
diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx index c2df8dea..59ababbf 100644 --- a/src/components/production/WorkOrders/WorkOrderDetail.tsx +++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx @@ -256,13 +256,14 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { onClick={() => handleStatusChange('in_progress')} disabled={isStatusUpdating} className="bg-green-600 hover:bg-green-700" + size="sm" > {isStatusUpdating ? ( - + ) : ( - + )} - 작업 시작 + 작업 시작 )} {order.status === 'in_progress' && ( @@ -272,25 +273,27 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { onClick={() => handleStatusChange('waiting')} disabled={isStatusUpdating} className="text-muted-foreground hover:text-foreground" + size="sm" > {isStatusUpdating ? ( - + ) : ( - + )} - 작업 취소 + 작업 취소 )} @@ -300,25 +303,27 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { onClick={() => handleStatusChange('in_progress')} disabled={isStatusUpdating} className="text-orange-600 hover:text-orange-700 border-orange-300 hover:bg-orange-50" + size="sm" > {isStatusUpdating ? ( - + ) : ( - + )} - 되돌리기 + 되돌리기 )} - ); @@ -331,25 +336,25 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { return (
{/* 기본 정보 (기획서 4열 구성) */} -
+

기본 정보

-
+
{/* 1행: 작업번호 | 수주일 | 공정구분 | 로트번호 */} -
+

작업번호

-

{order.workOrderNo}

+

{order.workOrderNo}

-
+

수주일

{order.salesOrderDate || '-'}

-
+

공정구분

{order.processName}

-
+

로트번호

-

{order.lotNo}

+

{order.lotNo}

{/* 2행: 수주처 | 현장명 | 수주 담당자 | 담당자 연락처 */} diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index 687b3d5e..fe990fa2 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -390,14 +390,14 @@ export function InspectionDetail({ id }: InspectionDetailProps) { if (isEditMode || !inspection) return null; return ( -
+ <> {inspection.status !== '완료' && ( )} -
+ ); }, [isEditMode, inspection]); diff --git a/src/components/quotes/QuoteFooterBar.tsx b/src/components/quotes/QuoteFooterBar.tsx index 81b15b64..da0319ad 100644 --- a/src/components/quotes/QuoteFooterBar.tsx +++ b/src/components/quotes/QuoteFooterBar.tsx @@ -70,38 +70,40 @@ export function QuoteFooterBar({ }: QuoteFooterBarProps) { return (
-
+
{/* 왼쪽: 뒤로가기 + 금액 표시 */} -
+
-

예상 전체 견적금액

-

+

예상 전체 견적금액

+

{totalAmount.toLocaleString()} - +

{/* 오른쪽: 버튼들 */} -
+
{/* 견적서 보기 */} {/* 거래명세서 보기 */} @@ -109,10 +111,11 @@ export function QuoteFooterBar({ onClick={onTransactionView} disabled={totalLocations === 0} variant="outline" - className="gap-2 px-6" + size="sm" + className="md:size-default md:px-6" > - - 거래명세서 보기 + + 거래명세서 보기 {/* 수식보기 - 개발환경(local/development)에서만 표시 */} @@ -121,10 +124,11 @@ export function QuoteFooterBar({ onClick={onFormulaView} disabled={!hasBomResult} variant="outline" - className="gap-2 px-6 border-purple-300 text-purple-600 hover:bg-purple-50" + size="sm" + className="border-purple-300 text-purple-600 hover:bg-purple-50 md:size-default md:px-6" > - - 수식보기 + + 수식보기 )} @@ -133,10 +137,11 @@ export function QuoteFooterBar({ )} @@ -146,10 +151,11 @@ export function QuoteFooterBar({ onClick={onDiscount} disabled={isViewMode} variant="outline" - className="gap-2 px-6 border-orange-300 text-orange-600 hover:bg-orange-50" + size="sm" + className="border-orange-300 text-orange-600 hover:bg-orange-50 md:size-default md:px-6" > - - 할인하기 + + 할인하기 )} @@ -158,14 +164,15 @@ export function QuoteFooterBar({ )} @@ -174,14 +181,15 @@ export function QuoteFooterBar({ )} @@ -189,10 +197,11 @@ export function QuoteFooterBar({ {status === "final" && onOrderRegister && ( )}
diff --git a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx index 9de85bbc..956a5593 100644 --- a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx +++ b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx @@ -625,35 +625,32 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
{/* 하단 액션 버튼 (sticky) */} -
- -
+
{isNew ? ( - ) : ( <> - @@ -662,17 +659,20 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi reloadPermissions(); toast.success('권한 정보가 저장되었습니다.'); }} + size="sm" + className="md:size-default" > - - 권한 정보 저장 + + 권한 정보 저장 )} diff --git a/src/layouts/AuthenticatedLayout.tsx b/src/layouts/AuthenticatedLayout.tsx index d96999e9..584f5e1a 100644 --- a/src/layouts/AuthenticatedLayout.tsx +++ b/src/layouts/AuthenticatedLayout.tsx @@ -101,8 +101,8 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro const router = useRouter(); const pathname = usePathname(); // 현재 경로 추적 - // 폰트 크기 조절 (12~18px, 기본 16px) - const FONT_SIZES = [12, 13, 14, 15, 16] as const; + // 폰트 크기 조절 (12~20px, 기본 16px) + const FONT_SIZES = [12, 13, 14, 15, 16, 17, 18, 19, 20] as const; const [fontSize, setFontSize] = useState(16); useEffect(() => { @@ -110,7 +110,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro const saved = localStorage.getItem('sam-font-size'); if (saved) { const size = parseInt(saved, 10); - if (size >= 12 && size <= 18) { + if (size >= 12 && size <= 20) { setFontSize(size); document.documentElement.style.setProperty('--font-size', `${size}px`); } @@ -118,7 +118,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro }, []); const handleFontSizeChange = useCallback((size: number) => { - const clamped = Math.max(12, Math.min(18, size)); + const clamped = Math.max(12, Math.min(20, size)); setFontSize(clamped); document.documentElement.style.setProperty('--font-size', `${clamped}px`); localStorage.setItem('sam-font-size', String(clamped)); @@ -929,6 +929,38 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro {/* 구분선 */}
+ {/* 글자 크기 조절 */} +
+
+ + 글자 크기 +
+ + {fontSize}px + +
+
+
+ + {/* 구분선 */} +
+ {/* 로그아웃 */} @@ -1180,46 +1212,30 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro {/* 글자 크기 조절 */}
-
- - 글자 크기 - {fontSize}px -
-
- -
- {FONT_SIZES.map((size) => ( - - ))} +
+ + 글자 크기 +
+ + {fontSize}px +
-