summaryrefslogtreecommitdiff
path: root/lib/basic-contract/vendor-table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-27 12:06:26 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-27 12:06:26 +0000
commit7548e2ad6948f1c6aa102fcac408bc6c9c0f9796 (patch)
tree8e66703ec821888ad51dcc242a508813a027bf71 /lib/basic-contract/vendor-table
parent7eac558470ef179dad626a8e82db5784fe86a556 (diff)
(대표님, 최겸) 기본계약, 입찰, 파일라우트, 계약서명라우트, 인포메이션, 메뉴설정, PQ(메일템플릿 관련)
Diffstat (limited to 'lib/basic-contract/vendor-table')
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx631
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-table.tsx2
-rw-r--r--lib/basic-contract/vendor-table/survey-conditional.ts180
3 files changed, 474 insertions, 339 deletions
diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
index 319ae4b9..f70bed94 100644
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -22,14 +22,15 @@ import {
Loader2,
ArrowRight,
Trophy,
- Target
+ Target,
+ Shield
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { useRouter } from "next/navigation"
import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer";
-import { getVendorAttachments } from "../service";
+import { getVendorAttachments, processBuyerSignatureAction } from "../service";
// 계약서 상태 타입 정의
interface ContractStatus {
@@ -42,16 +43,27 @@ interface BasicContractSignDialogProps {
contracts: BasicContractView[];
onSuccess?: () => void;
hasSelectedRows?: boolean;
+ mode?: 'vendor' | 'buyer';
+ onBuyerSignComplete?: (contractId: number, signedData: ArrayBuffer) => void;
t: (key: string) => string;
+ // 외부 상태 제어를 위한 새로운 props (선택적)
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
}
export function BasicContractSignDialog({
contracts,
onSuccess,
hasSelectedRows = false,
- t
+ mode = 'vendor',
+ onBuyerSignComplete,
+ t,
+ // 새로 추가된 props
+ open: externalOpen,
+ onOpenChange: externalOnOpenChange
}: BasicContractSignDialogProps) {
- const [open, setOpen] = React.useState(false);
+ // 내부 상태 (외부 제어가 없을 때 사용)
+ const [internalOpen, setInternalOpen] = React.useState(false);
const [selectedContract, setSelectedContract] = React.useState<BasicContractView | null>(null);
const [instance, setInstance] = React.useState<null | WebViewerInstance>(null);
const [searchTerm, setSearchTerm] = React.useState("");
@@ -64,14 +76,21 @@ export function BasicContractSignDialog({
// 계약서 상태 관리
const [contractStatuses, setContractStatuses] = React.useState<ContractStatus[]>([]);
- // 🔥 새로 추가: 서명/설문 완료 상태 관리
+ // 서명/설문/GTC 코멘트 완료 상태 관리
const [surveyCompletionStatus, setSurveyCompletionStatus] = React.useState<Record<number, boolean>>({});
const [signatureStatus, setSignatureStatus] = React.useState<Record<number, boolean>>({});
+ const [gtcCommentStatus, setGtcCommentStatus] = React.useState<Record<number, { hasComments: boolean; commentCount: number }>>({});
const router = useRouter()
- console.log(selectedContract,"selectedContract")
- console.log(additionalFiles,"additionalFiles")
+ // 실제 사용할 open 상태 (외부 제어가 있으면 외부 상태 사용, 없으면 내부 상태 사용)
+ const isControlledExternally = externalOpen !== undefined;
+ const open = isControlledExternally ? externalOpen : internalOpen;
+
+ // 모드에 따른 텍스트
+ const isBuyerMode = mode === 'buyer';
+ const dialogTitle = isBuyerMode ? "구매자 최종승인 서명" : t("basicContracts.dialog.title");
+ const signButtonText = isBuyerMode ? "최종승인 완료" : "서명 완료 및 저장";
// 버튼 비활성화 조건
const isButtonDisabled = !hasSelectedRows || contracts.length === 0;
@@ -87,29 +106,28 @@ export function BasicContractSignDialog({
return "";
};
- // 🔥 현재 선택된 계약서의 서명 완료 가능 여부 확인
+ // 현재 선택된 계약서의 서명 완료 가능 여부 확인
const canCompleteCurrentContract = React.useMemo(() => {
if (!selectedContract) return false;
const contractId = selectedContract.id;
+
+ // 구매자 모드에서는 설문조사나 GTC 체크 불필요
+ if (isBuyerMode) {
+ const signatureCompleted = signatureStatus[contractId] === true;
+ return signatureCompleted;
+ }
+
+ // 협력업체 모드의 기존 로직
const isComplianceTemplate = selectedContract.templateName?.includes('준법');
+ const isGTCTemplate = selectedContract.templateName?.includes('GTC');
- // 1. 준법 템플릿인 경우 설문조사 완료 여부 확인
const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
-
- // 2. 서명 완료 여부 확인
+ const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true;
const signatureCompleted = signatureStatus[contractId] === true;
- console.log('🔍 서명 완료 가능 여부 체크:', {
- contractId,
- isComplianceTemplate,
- surveyCompleted,
- signatureCompleted,
- canComplete: surveyCompleted && signatureCompleted
- });
-
- return surveyCompleted && signatureCompleted;
- }, [selectedContract, surveyCompletionStatus, signatureStatus]);
+ return surveyCompleted && gtcCompleted && signatureCompleted;
+ }, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]);
// 계약서별 상태 초기화
React.useEffect(() => {
@@ -142,7 +160,7 @@ export function BasicContractSignDialog({
return contracts.find(contract => contract.id === nextPendingId) || null;
};
- // 다이얼로그 열기/닫기 핸들러
+ // 다이얼로그 열기/닫기 핸들러 (외부 제어 지원)
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen && !allCompleted && completedCount > 0) {
// 완료되지 않은 계약서가 있으면 확인 대화상자
@@ -152,7 +170,12 @@ export function BasicContractSignDialog({
if (!confirmClose) return;
}
- setOpen(isOpen);
+ // 외부 제어가 있으면 외부 콜백 호출, 없으면 내부 상태 업데이트
+ if (isControlledExternally && externalOnOpenChange) {
+ externalOnOpenChange(isOpen);
+ } else {
+ setInternalOpen(isOpen);
+ }
if (!isOpen) {
// 다이얼로그 닫을 때 상태 초기화
@@ -160,8 +183,9 @@ export function BasicContractSignDialog({
setSearchTerm("");
setAdditionalFiles([]);
setContractStatuses([]);
- setSurveyCompletionStatus({}); // 🔥 추가
- setSignatureStatus({}); // 🔥 추가
+ setSurveyCompletionStatus({});
+ setSignatureStatus({});
+ setGtcCommentStatus({});
// WebViewer 인스턴스 정리
if (instance) {
try {
@@ -202,9 +226,14 @@ export function BasicContractSignDialog({
}
}
}, [open, contracts, selectedContract, contractStatuses]);
-
- // 추가 파일 가져오기 useEffect
+
+ // 추가 파일 가져오기 useEffect (구매자 모드에서는 스킵)
React.useEffect(() => {
+ if (isBuyerMode) {
+ setAdditionalFiles([]);
+ return;
+ }
+
const fetchAdditionalFiles = async () => {
if (!selectedContract) {
setAdditionalFiles([]);
@@ -235,9 +264,9 @@ export function BasicContractSignDialog({
};
fetchAdditionalFiles();
- }, [selectedContract]);
+ }, [selectedContract, isBuyerMode]);
- // 🔥 설문조사 완료 콜백 함수
+ // 설문조사 완료 콜백 함수
const handleSurveyComplete = React.useCallback((contractId: number) => {
console.log(`📋 설문조사 완료: 계약서 ${contractId}`);
setSurveyCompletionStatus(prev => ({
@@ -246,7 +275,7 @@ export function BasicContractSignDialog({
}));
}, []);
- // 🔥 서명 완료 콜백 함수
+ // 서명 완료 콜백 함수
const handleSignatureComplete = React.useCallback((contractId: number) => {
console.log(`✍️ 서명 완료: 계약서 ${contractId}`);
setSignatureStatus(prev => ({
@@ -255,31 +284,64 @@ export function BasicContractSignDialog({
}));
}, []);
- // 서명 완료 핸들러 (수정됨)
+ // GTC 코멘트 상태 변경 콜백 함수
+ const handleGtcCommentStatusChange = React.useCallback((contractId: number, hasComments: boolean, commentCount: number) => {
+ console.log(`📋 GTC 코멘트 상태 변경: 계약서 ${contractId}, 코멘트 ${commentCount}개`);
+ setGtcCommentStatus(prev => ({
+ ...prev,
+ [contractId]: { hasComments, commentCount }
+ }));
+ }, []);
+
+ // 서명 완료 핸들러
const completeSign = async () => {
if (!instance || !selectedContract) return;
- // 🔥 서명 완료 가능 여부 재확인
+ // 서명 완료 가능 여부 재확인
if (!canCompleteCurrentContract) {
const contractId = selectedContract.id;
- const isComplianceTemplate = selectedContract.templateName?.includes('준법');
- const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
- const signatureCompleted = signatureStatus[contractId] === true;
- if (!surveyCompleted) {
- toast.error("준법 설문조사를 먼저 완료해주세요.", {
- description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.",
- icon: <AlertCircle className="h-5 w-5 text-red-500" />
- });
- return;
- }
-
- if (!signatureCompleted) {
- toast.error("계약서에 서명을 먼저 완료해주세요.", {
- description: "문서의 서명 필드에 서명해주세요.",
- icon: <Target className="h-5 w-5 text-blue-500" />
- });
- return;
+ if (isBuyerMode) {
+ const signatureCompleted = signatureStatus[contractId] === true;
+
+ if (!signatureCompleted) {
+ toast.error("계약서에 서명을 먼저 완료해주세요.", {
+ description: "문서의 서명 필드에 서명해주세요.",
+ icon: <Target className="h-5 w-5 text-blue-500" />
+ });
+ return;
+ }
+ } else {
+ // 협력업체 모드의 기존 검증 로직
+ const isComplianceTemplate = selectedContract.templateName?.includes('준법');
+ const isGTCTemplate = selectedContract.templateName?.includes('GTC');
+ const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
+ const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true;
+ const signatureCompleted = signatureStatus[contractId] === true;
+
+ if (!surveyCompleted) {
+ toast.error("준법 설문조사를 먼저 완료해주세요.", {
+ description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.",
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ });
+ return;
+ }
+
+ if (!gtcCompleted) {
+ toast.error("GTC 조항에 코멘트가 있어 서명할 수 없습니다.", {
+ description: "조항 검토 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.",
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ });
+ return;
+ }
+
+ if (!signatureCompleted) {
+ toast.error("계약서에 서명을 먼저 완료해주세요.", {
+ description: "문서의 서명 필드에 서명해주세요.",
+ icon: <Target className="h-5 w-5 text-blue-500" />
+ });
+ return;
+ }
}
return;
@@ -303,73 +365,135 @@ export function BasicContractSignDialog({
xfdfString,
downloadType: "pdf",
});
-
- // FormData 생성 및 파일 추가
- const submitFormData = new FormData();
- submitFormData.append('file', new Blob([data], { type: 'application/pdf' }));
- submitFormData.append('tableRowId', selectedContract.id.toString());
- submitFormData.append('templateName', selectedContract.signedFileName || '');
-
- // 폼 필드 데이터 추가
- if (Object.keys(formData).length > 0) {
- submitFormData.append('formData', JSON.stringify(formData));
- }
-
- // API 호출
- const response = await fetch('/api/upload/signed-contract', {
- method: 'POST',
- body: submitFormData,
- next: { tags: ["basicContractView-vendor"] },
- });
-
- const result = await response.json();
-
- if (result.result) {
- // 성공시 해당 계약서 상태를 완료로 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'completed' as const }
- : status
- )
+
+ if (isBuyerMode) {
+ // 구매자 모드: 최종승인 처리
+ const result = await processBuyerSignatureAction(
+ selectedContract.id,
+ data,
+ selectedContract.signedFileName || `contract_${selectedContract.id}_buyer_signed.pdf`
);
- toast.success("계약서 서명이 완료되었습니다!", {
- description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`,
- icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
- });
+ if (result.success) {
+ // 성공시 해당 계약서 상태를 완료로 업데이트
+ setContractStatuses(prev =>
+ prev.map(status =>
+ status.id === selectedContract.id
+ ? { ...status, status: 'completed' as const }
+ : status
+ )
+ );
- // 다음 미완료 계약서로 자동 이동
- const nextContract = getNextPendingContract();
- if (nextContract) {
- setSelectedContract(nextContract);
- toast.info(`다음 계약서로 이동합니다`, {
- description: nextContract.templateName,
- icon: <ArrowRight className="h-4 w-4 text-blue-500" />
+ toast.success("최종승인이 완료되었습니다!", {
+ description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`,
+ icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
});
+
+ // 구매자 서명 완료 콜백 호출
+ if (onBuyerSignComplete) {
+ onBuyerSignComplete(selectedContract.id, data);
+ }
+
+ // 다음 미완료 계약서로 자동 이동
+ const nextContract = getNextPendingContract();
+ if (nextContract) {
+ setSelectedContract(nextContract);
+ toast.info(`다음 계약서로 이동합니다`, {
+ description: nextContract.templateName,
+ icon: <ArrowRight className="h-4 w-4 text-blue-500" />
+ });
+ } else {
+ // 모든 계약서 완료시
+ toast.success("🎉 모든 계약서 최종승인이 완료되었습니다!", {
+ description: `총 ${totalCount}개 계약서 승인 완료`,
+ icon: <Trophy className="h-5 w-5 text-yellow-500" />
+ });
+ }
+
+ router.refresh();
} else {
- // 모든 계약서 완료시
- toast.success("🎉 모든 계약서 서명이 완료되었습니다!", {
- description: `총 ${totalCount}개 계약서 서명 완료`,
- icon: <Trophy className="h-5 w-5 text-yellow-500" />
+ // 실패시 에러 상태 업데이트
+ setContractStatuses(prev =>
+ prev.map(status =>
+ status.id === selectedContract.id
+ ? { ...status, status: 'error' as const, errorMessage: result.message }
+ : status
+ )
+ );
+
+ toast.error("최종승인 처리 중 오류가 발생했습니다", {
+ description: result.message,
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
});
}
-
- router.refresh();
} else {
- // 실패시 에러 상태 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'error' as const, errorMessage: result.error }
- : status
- )
- );
-
- toast.error("서명 처리 중 오류가 발생했습니다", {
- description: result.error,
- icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ // 협력업체 모드: 기존 로직
+ const submitFormData = new FormData();
+ submitFormData.append('file', new Blob([data], { type: 'application/pdf' }));
+ submitFormData.append('tableRowId', selectedContract.id.toString());
+ submitFormData.append('templateName', selectedContract.signedFileName || '');
+
+ // 폼 필드 데이터 추가
+ if (Object.keys(formData).length > 0) {
+ submitFormData.append('formData', JSON.stringify(formData));
+ }
+
+ // API 호출
+ const response = await fetch('/api/upload/signed-contract', {
+ method: 'POST',
+ body: submitFormData,
+ next: { tags: ["basicContractView-vendor"] },
});
+
+ const result = await response.json();
+
+ if (result.result) {
+ // 성공시 해당 계약서 상태를 완료로 업데이트
+ setContractStatuses(prev =>
+ prev.map(status =>
+ status.id === selectedContract.id
+ ? { ...status, status: 'completed' as const }
+ : status
+ )
+ );
+
+ toast.success("계약서 서명이 완료되었습니다!", {
+ description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`,
+ icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
+ });
+
+ // 다음 미완료 계약서로 자동 이동
+ const nextContract = getNextPendingContract();
+ if (nextContract) {
+ setSelectedContract(nextContract);
+ toast.info(`다음 계약서로 이동합니다`, {
+ description: nextContract.templateName,
+ icon: <ArrowRight className="h-4 w-4 text-blue-500" />
+ });
+ } else {
+ // 모든 계약서 완료시
+ toast.success("🎉 모든 계약서 서명이 완료되었습니다!", {
+ description: `총 ${totalCount}개 계약서 서명 완료`,
+ icon: <Trophy className="h-5 w-5 text-yellow-500" />
+ });
+ }
+
+ router.refresh();
+ } else {
+ // 실패시 에러 상태 업데이트
+ setContractStatuses(prev =>
+ prev.map(status =>
+ status.id === selectedContract.id
+ ? { ...status, status: 'error' as const, errorMessage: result.error }
+ : status
+ )
+ );
+
+ toast.error("서명 처리 중 오류가 발생했습니다", {
+ description: result.error,
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ });
+ }
}
} catch (error) {
console.error("서명 완료 중 오류:", error);
@@ -391,56 +515,99 @@ export function BasicContractSignDialog({
// 모든 서명 완료 핸들러
const completeAllSigns = () => {
- setOpen(false);
+ handleOpenChange(false);
if (onSuccess) {
onSuccess();
}
- toast.success("모든 계약서 서명이 완료되었습니다!", {
- description: "계약서 관리 페이지가 새로고침됩니다.",
+ const successMessage = isBuyerMode
+ ? "모든 계약서 최종승인이 완료되었습니다!"
+ : "모든 계약서 서명이 완료되었습니다!";
+
+ toast.success(successMessage, {
+ description: "계약서 관리 페이지가 새고침됩니다.",
icon: <Trophy className="h-5 w-5 text-yellow-500" />
});
};
return (
<>
- {/* 서명 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() => setOpen(true)}
- disabled={isButtonDisabled}
- className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed"
- >
- <Upload
- className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`}
- aria-hidden="true"
- />
- <span className="hidden sm:inline flex items-center">
- {t("basicContracts.toolbar.sign")}
- {contracts.length > 0 && !isButtonDisabled && (
- <Badge variant="secondary" className="ml-2 bg-blue-100 text-blue-700 hover:bg-blue-200">
- {contracts.length}
- </Badge>
+ {/* 서명 버튼 - 외부 제어가 없을 때만 표시 */}
+ {!isControlledExternally && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleOpenChange(true)}
+ disabled={isButtonDisabled}
+ className={cn(
+ "gap-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed",
+ isBuyerMode
+ ? "hover:bg-green-50 hover:text-green-600 hover:border-green-200"
+ : "hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200"
)}
- {isButtonDisabled && (
- <span className="ml-2 text-xs text-gray-400">
- ({getDisabledReason()})
- </span>
+ >
+ {isBuyerMode ? (
+ <Shield
+ className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-green-500'}`}
+ aria-hidden="true"
+ />
+ ) : (
+ <Upload
+ className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`}
+ aria-hidden="true"
+ />
)}
- </span>
- </Button>
+ <span className="hidden sm:inline flex items-center">
+ {isBuyerMode ? "구매자 승인" : t("basicContracts.toolbar.sign")}
+ {contracts.length > 0 && !isButtonDisabled && (
+ <Badge
+ variant="secondary"
+ className={cn(
+ "ml-2",
+ isBuyerMode
+ ? "bg-green-100 text-green-700 hover:bg-green-200"
+ : "bg-blue-100 text-blue-700 hover:bg-blue-200"
+ )}최
+ >
+ {contracts.length}
+ </Badge>
+ )}
+ {isButtonDisabled && (
+ <span className="ml-2 text-xs text-gray-400">
+ ({getDisabledReason()})
+ </span>
+ )}
+ </span>
+ </Button>
+ )}
{/* 서명 다이얼로그 */}
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{width:'95vw', maxWidth:'95vw'}}>
{/* 고정 헤더 - 진행 상황 표시 */}
- <DialogHeader className="px-6 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b flex-shrink-0">
+ <DialogHeader className={cn(
+ "px-6 py-4 border-b flex-shrink-0",
+ isBuyerMode
+ ? "bg-gradient-to-r from-green-50 to-emerald-50"
+ : "bg-gradient-to-r from-blue-50 to-purple-50"
+ )}>
<DialogTitle className="text-xl font-bold flex items-center justify-between text-gray-800">
<div className="flex items-center">
- <FileSignature className="mr-2 h-5 w-5 text-blue-500" />
- {t("basicContracts.dialog.title")}
+ {isBuyerMode ? (
+ <Shield className="mr-2 h-5 w-5 text-green-500" />
+ ) : (
+ <FileSignature className="mr-2 h-5 w-5 text-blue-500" />
+ )}
+ {dialogTitle}
{/* 진행 상황 표시 */}
- <Badge variant="outline" className="ml-3 bg-blue-50 text-blue-700 border-blue-200">
+ <Badge
+ variant="outline"
+ className={cn(
+ "ml-3",
+ isBuyerMode
+ ? "bg-green-50 text-green-700 border-green-200"
+ : "bg-blue-50 text-blue-700 border-blue-200"
+ )}
+ >
{completedCount}/{totalCount} 완료
</Badge>
{/* 추가 파일 로딩 표시 */}
@@ -466,7 +633,12 @@ export function BasicContractSignDialog({
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
- className="bg-gradient-to-r from-blue-500 to-green-500 h-2 rounded-full transition-all duration-500"
+ className={cn(
+ "h-2 rounded-full transition-all duration-500",
+ isBuyerMode
+ ? "bg-gradient-to-r from-green-500 to-emerald-500"
+ : "bg-gradient-to-r from-blue-500 to-green-500"
+ )}
style={{ width: `${(completedCount / totalCount) * 100}%` }}
/>
</div>
@@ -506,9 +678,11 @@ export function BasicContractSignDialog({
const isCompleted = contractStatus?.status === 'completed';
const hasError = contractStatus?.status === 'error';
- // 🔥 계약서별 완료 상태 확인
+ // 계약서별 완료 상태 확인
const isComplianceTemplate = contract.templateName?.includes('준법');
+ const isGTCTemplate = contract.templateName?.includes('GTC');
const hasSurveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contract.id] === true : true;
+ const hasGtcCompleted = isGTCTemplate ? (gtcCommentStatus[contract.id]?.hasComments !== true) : true;
const hasSignatureCompleted = signatureStatus[contract.id] === true;
return (
@@ -530,7 +704,11 @@ export function BasicContractSignDialog({
{/* 첫 번째 줄: 제목 + 상태 */}
<div className="flex items-center justify-between w-full">
<span className="font-medium text-xs truncate text-gray-800 flex items-center min-w-0">
- <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-0" />
+ {isBuyerMode ? (
+ <Shield className="h-3 w-3 mr-1 text-green-500 flex-shrink-0" />
+ ) : (
+ <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-0" />
+ )}
<span className="truncate">{contract.templateName || t("basicContracts.dialog.document")}</span>
{/* 비밀유지 계약서인 경우 표시 */}
{contract.templateName === "비밀유지 계약서" && (
@@ -538,6 +716,12 @@ export function BasicContractSignDialog({
NDA
</Badge>
)}
+ {/* GTC 계약서인 경우 표시 */}
+ {contract.templateName?.includes('GTC') && (
+ <Badge variant="outline" className="ml-1 bg-purple-50 text-purple-700 border-purple-200 text-xs">
+ GTC
+ </Badge>
+ )}
</span>
{/* 상태 표시 */}
@@ -558,8 +742,8 @@ export function BasicContractSignDialog({
)}
</div>
- {/* 🔥 완료 상태 표시 */}
- {!isCompleted && !hasError && (
+ {/* 완료 상태 표시 (구매자 모드에서는 간소화) */}
+ {!isCompleted && !hasError && !isBuyerMode && (
<div className="flex items-center space-x-2 text-xs">
{isComplianceTemplate && (
<span className={`flex items-center ${hasSurveyCompleted ? 'text-green-600' : 'text-gray-400'}`}>
@@ -567,6 +751,12 @@ export function BasicContractSignDialog({
설문
</span>
)}
+ {isGTCTemplate && (
+ <span className={`flex items-center ${hasGtcCompleted ? 'text-green-600' : 'text-red-600'}`}>
+ <CheckCircle2 className={`h-3 w-3 mr-1 ${hasGtcCompleted ? 'text-green-500' : 'text-red-500'}`} />
+ 조항검토
+ </span>
+ )}
<span className={`flex items-center ${hasSignatureCompleted ? 'text-green-600' : 'text-gray-400'}`}>
<Target className={`h-3 w-3 mr-1 ${hasSignatureCompleted ? 'text-green-500' : 'text-gray-300'}`} />
서명
@@ -574,6 +764,16 @@ export function BasicContractSignDialog({
</div>
)}
+ {/* 구매자 모드의 간소화된 상태 표시 */}
+ {!isCompleted && !hasError && isBuyerMode && (
+ <div className="flex items-center space-x-2 text-xs">
+ <span className={`flex items-center ${hasSignatureCompleted ? 'text-green-600' : 'text-gray-400'}`}>
+ <Target className={`h-3 w-3 mr-1 ${hasSignatureCompleted ? 'text-green-500' : 'text-gray-300'}`} />
+ 구매자 서명
+ </span>
+ </div>
+ )}
+
{/* 두 번째 줄: 사용자 + 날짜 */}
<div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center min-w-0">
@@ -609,14 +809,18 @@ export function BasicContractSignDialog({
{/* 뷰어 헤더 */}
<div className="p-4 border-b bg-gray-50 flex-shrink-0">
<h3 className="font-semibold text-gray-800 flex items-center">
- <FileText className="h-4 w-4 mr-2 text-blue-500" />
+ {isBuyerMode ? (
+ <Shield className="h-4 w-4 mr-2 text-green-500" />
+ ) : (
+ <FileText className="h-4 w-4 mr-2 text-blue-500" />
+ )}
{selectedContract.templateName || t("basicContracts.dialog.document")}
{/* 현재 계약서 상태 표시 */}
{currentContractStatus?.status === 'completed' ? (
<Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
<CheckCircle2 className="h-3 w-3 mr-1" />
- 서명 완료
+ {isBuyerMode ? "승인 완료" : "서명 완료"}
</Badge>
) : currentContractStatus?.status === 'error' ? (
<Badge variant="outline" className="ml-2 bg-red-50 text-red-700 border-red-200">
@@ -625,19 +829,32 @@ export function BasicContractSignDialog({
</Badge>
) : (
<Badge variant="outline" className="ml-2 bg-yellow-50 text-yellow-700 border-yellow-200">
- 서명 대기
+ {isBuyerMode ? "승인 대기" : "서명 대기"}
+ </Badge>
+ )}
+
+ {/* 구매자 모드 배지 */}
+ {isBuyerMode && (
+ <Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
+ 구매자 모드
</Badge>
)}
- {/* 준법 템플릿 표시 */}
- {selectedContract.templateName?.includes('준법') && (
+ {/* 준법/GTC 템플릿 표시 (구매자 모드가 아닐 때만) */}
+ {!isBuyerMode && selectedContract.templateName?.includes('준법') && (
<Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200">
준법 서류
</Badge>
)}
+
+ {!isBuyerMode && selectedContract.templateName?.includes('GTC') && (
+ <Badge variant="outline" className="ml-2 bg-purple-50 text-purple-700 border-purple-200">
+ GTC 계약서
+ </Badge>
+ )}
- {/* 비밀유지 계약서인 경우 추가 파일 수 표시 */}
- {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
+ {/* 비밀유지 계약서인 경우 추가 파일 수 표시 (구매자 모드가 아닐 때만) */}
+ {!isBuyerMode && selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
<Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200">
첨부파일 {additionalFiles.length}개
</Badge>
@@ -665,8 +882,12 @@ export function BasicContractSignDialog({
additionalFiles={additionalFiles}
instance={instance}
setInstance={setInstance}
- onSurveyComplete={() => handleSurveyComplete(selectedContract.id)} // 🔥 추가
- onSignatureComplete={() => handleSignatureComplete(selectedContract.id)} // 🔥 추가
+ onSurveyComplete={() => handleSurveyComplete(selectedContract.id)}
+ onSignatureComplete={() => handleSignatureComplete(selectedContract.id)}
+ onGtcCommentStatusChange={(hasComments, commentCount) =>
+ handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount)
+ }
+ mode={mode}
t={t}
/>
</div>
@@ -678,44 +899,58 @@ export function BasicContractSignDialog({
{currentContractStatus?.status === 'completed' ? (
<p className="text-sm text-green-600 flex items-center">
<CheckCircle2 className="h-4 w-4 text-green-500 mr-1" />
- 이 계약서는 이미 서명이 완료되었습니다
+ 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다
</p>
) : currentContractStatus?.status === 'error' ? (
<p className="text-sm text-red-600 flex items-center">
<AlertCircle className="h-4 w-4 text-red-500 mr-1" />
- 서명 처리 중 오류가 발생했습니다. 다시 시도해주세요.
+ {isBuyerMode ? "승인" : "서명"} 처리 중 오류가 발생했습니다. 다시 시도해주세요.
</p>
) : (
<>
- {/* 🔥 완료 조건 안내 메시지 개선 */}
+ {/* 완료 조건 안내 메시지 */}
<div className="flex flex-col space-y-1">
<p className="text-sm text-gray-600 flex items-center">
<AlertCircle className="h-4 w-4 text-yellow-500 mr-1" />
- {t("basicContracts.dialog.signWarning")}
+ {isBuyerMode
+ ? "계약서에 구매자 서명을 완료해주세요."
+ : t("basicContracts.dialog.signWarning")
+ }
</p>
{/* 완료 상태 체크리스트 */}
- <div className="flex items-center space-x-4 text-xs">
- {selectedContract.templateName?.includes('준법') && (
- <span className={`flex items-center ${surveyCompletionStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
- <CheckCircle2 className={`h-3 w-3 mr-1 ${surveyCompletionStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
- 설문조사 {surveyCompletionStatus[selectedContract.id] ? '완료' : '미완료'}
+ {!isBuyerMode && (
+ <div className="flex items-center space-x-4 text-xs">
+ {selectedContract.templateName?.includes('준법') && (
+ <span className={`flex items-center ${surveyCompletionStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
+ <CheckCircle2 className={`h-3 w-3 mr-1 ${surveyCompletionStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
+ 설문조사 {surveyCompletionStatus[selectedContract.id] ? '완료' : '미완료'}
+ </span>
+ )}
+ {selectedContract.templateName?.includes('GTC') && (
+ <span className={`flex items-center ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-600' : 'text-red-600'}`}>
+ <CheckCircle2 className={`h-3 w-3 mr-1 ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-500' : 'text-red-500'}`} />
+ 조항검토 {(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? '완료' :
+ `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`}
+ </span>
+ )}
+ <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
+ <Target className={`h-3 w-3 mr-1 ${signatureStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
+ 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
</span>
- )}
- <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
- <Target className={`h-3 w-3 mr-1 ${signatureStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
- 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
- </span>
- </div>
+ </div>
+ )}
+
+ {/* 구매자 모드의 간소화된 체크리스트 */}
+ {isBuyerMode && (
+ <div className="flex items-center space-x-4 text-xs">
+ <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
+ <Target className={`h-3 w-3 mr-1 ${signatureStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
+ 구매자 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
+ </span>
+ </div>
+ )}
</div>
-
- {/* 비밀유지 계약서인 경우 추가 안내 */}
- {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
- <p className="text-xs text-blue-600 flex items-center">
- <FileText className="h-3 w-3 text-blue-500 mr-1" />
- 첨부 서류도 확인해주세요
- </p>
- )}
</>
)}
</div>
@@ -725,11 +960,16 @@ export function BasicContractSignDialog({
{allCompleted ? (
// 모든 계약서 완료시
<Button
- className="gap-2 bg-green-600 hover:bg-green-700 transition-colors"
+ className={cn(
+ "gap-2 transition-colors",
+ isBuyerMode
+ ? "bg-green-600 hover:bg-green-700"
+ : "bg-green-600 hover:bg-green-700"
+ )}
onClick={completeAllSigns}
>
<Trophy className="h-4 w-4" />
- 모든 서명 완료
+ 모든 {isBuyerMode ? "승인" : "서명"} 완료
</Button>
) : currentContractStatus?.status === 'completed' ? (
// 현재 계약서가 완료된 경우
@@ -750,13 +990,16 @@ export function BasicContractSignDialog({
) : (
// 현재 계약서를 서명해야 하는 경우
<Button
- className={`gap-2 transition-colors ${
+ className={cn(
+ "gap-2 transition-colors",
canCompleteCurrentContract
- ? "bg-blue-600 hover:bg-blue-700"
+ ? isBuyerMode
+ ? "bg-green-600 hover:bg-green-700"
+ : "bg-blue-600 hover:bg-blue-700"
: "bg-gray-400 cursor-not-allowed"
- }`}
+ )}
onClick={completeSign}
- disabled={!canCompleteCurrentContract || isSubmitting} // 🔥 조건 수정
+ disabled={!canCompleteCurrentContract || isSubmitting}
>
{isSubmitting ? (
<>
@@ -768,11 +1011,15 @@ export function BasicContractSignDialog({
</>
) : (
<>
- <FileSignature className="h-4 w-4" />
- 서명 완료
+ {isBuyerMode ? (
+ <Shield className="h-4 w-4" />
+ ) : (
+ <FileSignature className="h-4 w-4" />
+ )}
+ {signButtonText}
{totalCount > 1 && (
<span className="ml-1 text-xs">
- ({completedCount + 1}/{totalCount})
+ ({completedCount}/{totalCount})
</span>
)}
</>
@@ -784,12 +1031,22 @@ export function BasicContractSignDialog({
</>
) : (
<div className="flex flex-col items-center justify-center h-full text-center p-6">
- <div className="bg-blue-50 p-6 rounded-full mb-4">
- <FileSignature className="h-12 w-12 text-blue-500" />
+ <div className={cn(
+ "p-6 rounded-full mb-4",
+ isBuyerMode ? "bg-green-50" : "bg-blue-50"
+ )}>
+ {isBuyerMode ? (
+ <Shield className="h-12 w-12 text-green-500" />
+ ) : (
+ <FileSignature className="h-12 w-12 text-blue-500" />
+ )}
</div>
<h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3>
<p className="text-gray-500 max-w-md">
- {t("basicContracts.dialog.selectDocumentDescription")}
+ {isBuyerMode
+ ? "승인할 계약서를 선택해주세요."
+ : t("basicContracts.dialog.selectDocumentDescription")
+ }
</p>
</div>
)}
diff --git a/lib/basic-contract/vendor-table/basic-contract-table.tsx b/lib/basic-contract/vendor-table/basic-contract-table.tsx
index 48298f21..cefc0fb2 100644
--- a/lib/basic-contract/vendor-table/basic-contract-table.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-table.tsx
@@ -38,6 +38,8 @@ export function BasicContractsVendorTable({ promises }: BasicTemplateTableProps)
const [{ data, pageCount }] =
React.use(promises)
+ console.log(data,"data")
+
// 안전한 번역 함수 (fallback 포함)
const safeT = React.useCallback((key: string, fallback: string) => {
diff --git a/lib/basic-contract/vendor-table/survey-conditional.ts b/lib/basic-contract/vendor-table/survey-conditional.ts
index 71c2c1ff..686cc4ba 100644
--- a/lib/basic-contract/vendor-table/survey-conditional.ts
+++ b/lib/basic-contract/vendor-table/survey-conditional.ts
@@ -131,6 +131,22 @@ public getIncompleteReason(question: SurveyQuestion, surveyAnswers: Record<numbe
return true;
}
+ private isValidFile(file: any): boolean {
+ if (!file || typeof file !== 'object') return false;
+
+ // 브라우저 File 객체 체크 (새로 업로드한 파일)
+ if (file.name || file.size || file.type) return true;
+
+ // 서버 파일 메타데이터 체크 (기존 파일)
+ if (file.filename || file.originalName || file.id || file.mimeType) return true;
+
+ return false;
+ }
+
+ private hasValidFiles(files: any[]): boolean {
+ return files && files.length > 0 && files.every(file => this.isValidFile(file));
+ }
+
private isQuestionCompleteEnhanced(question: SurveyQuestion, surveyAnswers: Record<number, any>): boolean {
const answer = surveyAnswers[question.id];
@@ -157,60 +173,50 @@ public getIncompleteReason(question: SurveyQuestion, surveyAnswers: Record<numbe
const hasOtherText = answer.answerValue === 'OTHER' ?
(answer.otherText && answer.otherText.trim() !== '') : true;
- // 4️⃣ files 체크 (실제 파일이 있는 경우)
- const hasValidFiles = answer.files && answer.files.length > 0 &&
- answer.files.every((file: any) => file && typeof file === 'object' &&
- (file.name || file.size || file.type)); // 빈 객체 {} 제외
+ // 4️⃣ files 체크 (브라우저 File 객체와 서버 파일 메타데이터 모두 처리) - 수정된 부분
+ const hasValidFilesResult = this.hasValidFiles(answer.files || []);
console.log(`📊 Q${question.questionNumber} 완료 조건 체크:`, {
hasAnswerValue,
hasDetailText,
hasOtherText,
- hasValidFiles,
+ hasValidFiles: hasValidFilesResult,
questionType: question.questionType,
hasDetailTextRequired: question.hasDetailText,
hasFileUploadRequired: question.hasFileUpload || question.questionType === 'FILE'
});
- // 🎯 질문 타입별 완료 조건
+ // 질문 타입별 완료 조건은 동일하지만 hasValidFilesResult 변수 사용
switch (question.questionType) {
case 'RADIO':
case 'DROPDOWN':
- // 선택형: answerValue가 있고, OTHER인 경우 otherText도 필요
const isSelectComplete = hasAnswerValue && hasOtherText;
- // detailText가 필요한 경우 추가 체크
if (question.hasDetailText && isSelectComplete) {
return hasDetailText;
}
- // 파일 업로드가 필요한 경우 추가 체크
if ((question.hasFileUpload || question.questionType === 'FILE') && isSelectComplete) {
- return hasValidFiles;
+ return hasValidFilesResult; // 수정된 부분
}
return isSelectComplete;
case 'TEXTAREA':
- // 텍스트 영역: detailText 또는 answerValue 중 하나라도 있으면 됨
return hasDetailText || hasAnswerValue;
case 'FILE':
- // 파일 업로드: 유효한 파일이 있어야 함
- return hasValidFiles;
+ return hasValidFilesResult; // 수정된 부분
default:
- // 기본: answerValue, detailText, 파일 중 하나라도 있으면 완료
- let isComplete = hasAnswerValue || hasDetailText || hasValidFiles;
+ let isComplete = hasAnswerValue || hasDetailText || hasValidFilesResult; // 수정된 부분
- // detailText가 필수인 경우
if (question.hasDetailText) {
isComplete = isComplete && hasDetailText;
}
- // 파일 업로드가 필수인 경우
if (question.hasFileUpload) {
- isComplete = isComplete && hasValidFiles;
+ isComplete = isComplete && hasValidFilesResult; // 수정된 부분
}
return isComplete;
@@ -303,17 +309,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any
// 🎯 현재 답변 상태에 따라 표시되는 질문들 계산
const visibleQuestions = this.getVisibleQuestions(surveyAnswers);
- console.log('🔍 표시되는 모든 질문들의 상세 정보:', visibleQuestions.map(q => ({
- id: q.id,
- questionNumber: q.questionNumber,
- questionText: q.questionText?.substring(0, 30) + '...',
- isRequired: q.isRequired,
- parentQuestionId: q.parentQuestionId,
- conditionalValue: q.conditionalValue,
- isConditional: !!q.parentQuestionId,
- hasAnswer: !!surveyAnswers[q.id]?.answerValue,
- answerValue: surveyAnswers[q.id]?.answerValue
- })));
+
// 🚨 중요: 트리거된 조건부 질문들을 필수로 처리
const requiredQuestions = visibleQuestions.filter(q => {
@@ -334,26 +330,6 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any
return false;
});
- console.log('📊 필수 질문 필터링 결과:', {
- 전체질문수: this.questions.length,
- 표시되는질문수: visibleQuestions.length,
- 원래필수질문: visibleQuestions.filter(q => q.isRequired).length,
- 트리거된조건부질문: visibleQuestions.filter(q => {
- if (!q.parentQuestionId || !q.conditionalValue) return false;
- const parentAnswer = surveyAnswers[q.parentQuestionId];
- return parentAnswer?.answerValue === q.conditionalValue;
- }).length,
- 최종필수질문: requiredQuestions.length,
- 현재답변수: Object.keys(surveyAnswers).length,
- 필수질문들: requiredQuestions.map(q => ({
- id: q.id,
- questionNumber: q.questionNumber,
- isRequired: q.isRequired,
- isConditional: !!q.parentQuestionId,
- hasAnswer: !!surveyAnswers[q.id]?.answerValue,
- 처리방식: q.isRequired ? '원래필수' : '트리거됨'
- }))
- });
const completedQuestionIds: number[] = [];
const incompleteQuestionIds: number[] = [];
@@ -365,10 +341,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any
// 🎯 개선된 완료 체크: 모든 답변 형태를 고려
const isComplete = this.isQuestionCompleteEnhanced(question, surveyAnswers);
-
- console.log(`📊 Q${question.questionNumber} 완료 상태: ${isComplete}`);
- console.log(`📝 Q${question.questionNumber} 답변 내용:`, surveyAnswers[question.id]);
-
+
// 디버깅 정보 수집
debugInfo[question.id] = {
questionText: question.questionText,
@@ -414,25 +387,6 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any
if (process.env.NODE_ENV === 'development') {
(result as any).debugInfo = debugInfo;
- // 📋 상세한 진행 상황 로그
- console.log('📋 최종 진행 상황:', {
- 총필수질문: requiredQuestions.length,
- 완료된질문: completedQuestionIds.length,
- 미완료질문: incompleteQuestionIds.length,
- 진행률: `${Math.round(progressPercentage)}%`,
- 기본질문: visibleQuestions.filter(q => !q.parentQuestionId).length,
- 조건부질문: visibleQuestions.filter(q => q.parentQuestionId).length,
- 완료된기본질문: completedQuestionIds.filter(id => !visibleQuestions.find(q => q.id === id)?.parentQuestionId).length,
- 완료된조건부질문: completedQuestionIds.filter(id => !!visibleQuestions.find(q => q.id === id)?.parentQuestionId).length,
- 필수질문상세: requiredQuestions.map(q => ({
- id: q.id,
- questionNumber: q.questionNumber,
- isRequired: q.isRequired,
- isConditional: !!q.parentQuestionId,
- isComplete: completedQuestionIds.includes(q.id)
- }))
- });
-
// 🔍 미완료 질문들의 구체적 이유
if (incompleteQuestionIds.length > 0) {
console.log('🔍 미완료 질문들:', incompleteQuestionIds.map(id => ({
@@ -446,24 +400,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any
// ⚡ 조건부 질문 활성화 및 완료 현황
const conditionalQuestions = visibleQuestions.filter(q => q.parentQuestionId);
- if (conditionalQuestions.length > 0) {
- console.log('⚡ 조건부 질문 상세 현황:', conditionalQuestions.map(q => ({
- id: q.id,
- questionNumber: q.questionNumber,
- isRequired: q.isRequired,
- parentId: q.parentQuestionId,
- condition: q.conditionalValue,
- parentAnswer: surveyAnswers[q.parentQuestionId!]?.answerValue,
- isTriggered: surveyAnswers[q.parentQuestionId!]?.answerValue === q.conditionalValue,
- hasAnswer: !!surveyAnswers[q.id]?.answerValue,
- answerValue: surveyAnswers[q.id]?.answerValue,
- detailText: surveyAnswers[q.id]?.detailText,
- files: surveyAnswers[q.id]?.files,
- isComplete: debugInfo[q.id]?.isComplete,
- isIncludedInRequired: requiredQuestions.some(rq => rq.id === q.id),
- completionDetails: this.getCompletionDetailsEnhanced(q, surveyAnswers[q.id])
- })));
- }
+
}
return result;
@@ -606,37 +543,21 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): {
const childQuestion = this.questions.find(q => q.id === childId);
if (!childQuestion) return;
- console.log(`🔍 자식 질문 ${childId} 체크:`, {
- childId,
- questionNumber: childQuestion.questionNumber,
- conditionalValue: childQuestion.conditionalValue,
- newParentValue,
- shouldKeep: childQuestion.conditionalValue === newParentValue,
- currentAnswer: updatedAnswers[childId]?.answerValue
- });
-
// 새로운 부모 값이 자식의 조건과 맞지 않으면 자식 답변 삭제
if (childQuestion.conditionalValue !== newParentValue) {
- console.log(`🗑️ 자식 질문 Q${childQuestion.questionNumber} 답변 초기화 (조건 불일치)`);
delete updatedAnswers[childId];
// 재귀적으로 손자 질문들도 정리
const grandChildAnswers = this.clearAffectedChildAnswers(childId, '', updatedAnswers);
Object.assign(updatedAnswers, grandChildAnswers);
} else {
- console.log(`✅ 자식 질문 Q${childQuestion.questionNumber} 유지 (조건 일치)`);
}
});
const clearedCount = childQuestionIds.filter(childId => !updatedAnswers[childId]).length;
const keptCount = childQuestionIds.filter(childId => !!updatedAnswers[childId]).length;
- console.log(`📊 자식 질문 정리 완료:`, {
- parentQuestionId,
- 총자식질문: childQuestionIds.length,
- 초기화된질문: clearedCount,
- 유지된질문: keptCount
- });
+
return updatedAnswers;
}
@@ -680,21 +601,17 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): {
conditionalValue: question.conditionalValue
};
- console.log(`🔍 질문 완료 체크 [Q${question.questionNumber}]:`, logData);
if (!question.isRequired) {
- console.log(`✅ Q${question.questionNumber}: 선택 질문이므로 완료`);
return true;
}
if (!answer?.answerValue) {
- console.log(`❌ Q${question.questionNumber}: 답변이 없음`);
return false;
}
// 1. '기타' 선택 시 추가 입력이 필요한 경우
if (answer.answerValue === 'OTHER' && !answer.otherText?.trim()) {
- console.log(`❌ Q${question.questionNumber}: '기타' 선택했지만 상세 내용 없음`);
return false;
}
@@ -702,16 +619,8 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): {
if (question.hasDetailText) {
const needsDetailText = ['YES', '네', 'Y', 'true', '1'].includes(answer.answerValue);
- console.log(`📝 Q${question.questionNumber} 상세텍스트 체크:`, {
- hasDetailText: question.hasDetailText,
- answerValue: answer.answerValue,
- needsDetailText,
- detailText: answer.detailText?.length || 0,
- detailTextExists: !!answer.detailText?.trim()
- });
if (needsDetailText && !answer.detailText?.trim()) {
- console.log(`❌ Q${question.questionNumber}: '${answer.answerValue}' 선택했지만 상세 내용 없음`);
return false;
}
}
@@ -720,17 +629,7 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): {
if (question.hasFileUpload || question.questionType === 'FILE') {
const needsFileUpload = ['YES', '네', 'Y', 'true', '1'].includes(answer.answerValue);
- console.log(`📁 Q${question.questionNumber} 파일업로드 체크:`, {
- hasFileUpload: question.hasFileUpload,
- questionType: question.questionType,
- answerValue: answer.answerValue,
- needsFileUpload,
- filesCount: answer.files?.length || 0,
- hasFiles: !!answer.files && answer.files.length > 0
- });
-
if (needsFileUpload && (!answer.files || answer.files.length === 0)) {
- console.log(`❌ Q${question.questionNumber}: '${answer.answerValue}' 선택했지만 파일 업로드 없음`);
return false;
}
}
@@ -739,50 +638,27 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): {
const childQuestions = this.getChildQuestions(question.id);
if (childQuestions.length > 0) {
- console.log(`🔗 Q${question.questionNumber} 부모 질문 - 자식 질문들:`,
- childQuestions.map(c => ({
- id: c.id,
- questionNumber: c.questionNumber,
- condition: c.conditionalValue,
- required: c.isRequired,
- text: c.questionText?.substring(0, 20) + '...'
- }))
- );
// 현재 답변으로 트리거되는 자식 질문들 찾기
const triggeredChildren = childQuestions.filter(child =>
child.conditionalValue === answer.answerValue
);
- console.log(`🎯 Q${question.questionNumber} 답변 '${answer.answerValue}'로 트리거된 자식들:`,
- triggeredChildren.map(c => ({
- id: c.id,
- questionNumber: c.questionNumber,
- required: c.isRequired,
- text: c.questionText?.substring(0, 30) + '...'
- }))
- );
-
// 트리거된 필수 자식 질문들이 모두 완료되었는지 확인
for (const childQuestion of triggeredChildren) {
if (childQuestion.isRequired) {
- console.log(`🔄 자식 질문 Q${childQuestion.questionNumber} 완료 체크 시작...`);
const childComplete = this.isQuestionComplete(childQuestion, surveyAnswers);
- console.log(`📊 자식 질문 Q${childQuestion.questionNumber} 완료 상태: ${childComplete}`);
if (!childComplete) {
- console.log(`❌ 부모 Q${question.questionNumber}의 자식 Q${childQuestion.questionNumber} 미완료`);
return false;
}
}
}
if (triggeredChildren.filter(c => c.isRequired).length > 0) {
- console.log(`✅ Q${question.questionNumber}의 모든 필수 조건부 자식 질문들 완료됨`);
}
}
- console.log(`✅ Q${question.questionNumber} 완료 체크 통과`);
return true;
}