diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-04 07:48:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-04 07:48:00 +0000 |
| commit | 680da9b323db8b8d7cf27c674ab0016ec87bfe81 (patch) | |
| tree | 52791f8618c0c5767c6420404ddf849ae28082e8 | |
| parent | 153502b67da990c92973f1f8af416f9a81ec3abb (diff) | |
(임수민) 구매 점검 테스트 요청사항 수정 (11/02)
| -rw-r--r-- | lib/evaluation/table/evaluation-columns.tsx | 54 | ||||
| -rw-r--r-- | lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx | 109 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-columns.tsx | 87 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 82 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table.tsx | 29 | ||||
| -rw-r--r-- | lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx | 70 |
6 files changed, 352 insertions, 79 deletions
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index b68aa70d..56029e08 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -133,7 +133,7 @@ const getDepartmentStatusBadge = (status: string | null) => { }; // 등급별 색상 -const getGradeBadgeVariant = (grade: string | null) => { +const getGradeBadgeVariant = (grade: string | null): "default" | "secondary" | "outline" | "destructive" => { if (!grade) return "outline"; switch (grade) { case "S": return "default"; @@ -264,7 +264,7 @@ export function getPeriodicEvaluationsColumns({ accessorKey: "vendorName", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, cell: ({ row }) => ( - <div className="truncate max-w-[200px]" title={row.original.vendorName}> + <div className="truncate max-w-[200px]" title={row.original.vendorName || undefined}> {row.original.vendorName} </div> ), @@ -381,7 +381,7 @@ export function getPeriodicEvaluationsColumns({ return finalGrade ? ( <Badge - variant={getGradeBadgeVariant(finalGrade)} + variant={getGradeBadgeVariant(finalGrade) || "outline"} className={isAggregated ? "bg-purple-600" : "bg-green-600"} > {finalGrade} @@ -803,18 +803,56 @@ export function getPeriodicEvaluationsColumns({ }, { - accessorKey: "evaluationGrade", + id: "evaluationGrade", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가등급" />, cell: ({ row }) => { - const grade = row.getValue<string>("evaluationGrade"); + // 확정된 등급이 있으면 우선 표시 + const finalGrade = row.original.finalGrade; const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; - return grade ? ( + if (finalGrade) { + return ( + <Badge + variant={getGradeBadgeVariant(finalGrade) || "outline"} + className={isAggregated ? "bg-purple-600" : ""} + > + {finalGrade}등급 + </Badge> + ); + } + + // 확정된 등급이 없으면 평가점수 기반으로 등급 계산 + const processScore = Number(row.getValue("processScore") || 0); + const priceScore = Number(row.getValue("priceScore") || 0); + const deliveryScore = Number(row.getValue("deliveryScore") || 0); + const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); + const participationBonus = Number(row.getValue("participationBonus") || 0); + const qualityDeduction = Number(row.getValue("qualityDeduction") || 0); + + const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; + const evaluationScore = totalScore + participationBonus - qualityDeduction; + + // 점수 기반으로 등급 계산 + // A: 95 이상, B: 90-95 미만, C: 60-90 미만, D: 60 미만 + let calculatedGrade: string | null = null; + if (evaluationScore > 0) { + if (evaluationScore >= 95) { + calculatedGrade = "A"; + } else if (evaluationScore >= 90) { + calculatedGrade = "B"; + } else if (evaluationScore >= 60) { + calculatedGrade = "C"; + } else { + calculatedGrade = "D"; + } + } + + return calculatedGrade ? ( <Badge - variant={getGradeBadgeVariant(grade)} + variant={getGradeBadgeVariant(calculatedGrade) || "outline"} className={isAggregated ? "bg-purple-600" : ""} > - {grade} + {calculatedGrade}등급 </Badge> ) : ( <span className="text-muted-foreground">-</span> diff --git a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx index 84651350..0e9efc8b 100644 --- a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx +++ b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx @@ -54,6 +54,37 @@ const calculateGrade = (score: number): "A" | "B" | "C" | "D" => { return "D" } +// 등급에 따른 점수 계산 (등급 변경 시 점수 자동 조정) +const calculateScoreFromGrade = (grade: "A" | "B" | "C" | "D"): number => { + switch (grade) { + case "A": + return 95 // A등급 최소 점수 + case "B": + return 90 // B등급 최소 점수 + case "C": + return 60 // C등급 최소 점수 + case "D": + return 0 // D등급은 0점으로 설정 (또는 30점 중간값) + default: + return 0 + } +} + +// 평가점수 계산 (evaluation-columns.tsx와 동일한 로직) +const calculateEvaluationScore = (evaluation: PeriodicEvaluationView): number => { + const processScore = Number(evaluation.processScore || 0); + const priceScore = Number(evaluation.priceScore || 0); + const deliveryScore = Number(evaluation.deliveryScore || 0); + const selfEvaluationScore = Number(evaluation.selfEvaluationScore || 0); + const participationBonus = Number(evaluation.participationBonus || 0); + const qualityDeduction = Number(evaluation.qualityDeduction || 0); + + const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; + const evaluationScore = totalScore + participationBonus - qualityDeduction; + + return evaluationScore; +} + // 개별 평가 스키마 const evaluationItemSchema = z.object({ id: z.number(), @@ -61,8 +92,8 @@ const evaluationItemSchema = z.object({ vendorCode: z.string(), evaluationScore: z.coerce.number().nullable(), finalScore: z.coerce.number() - .min(0, "점수는 0 이상이어야 합니다"), - // .max(100, "점수는 100 이하여야 합니다"), + .min(0, "점수는 0 이상이어야 합니다") + .max(100, "점수는 100 이하여야 합니다"), finalGrade: z.enum(["A", "B", "C", "D"]), }) @@ -103,14 +134,29 @@ export function FinalizeEvaluationDialog({ // evaluations가 변경될 때 폼 초기화 React.useEffect(() => { if (evaluations.length > 0) { - const formData = evaluations.map(evaluation => ({ - id: evaluation.id, - vendorName: evaluation.vendorName || "", - vendorCode: evaluation.vendorCode || "", - evaluationScore: evaluation.evaluationScore ? Number(evaluation.evaluationScore) : null, - finalScore: Number(evaluation.evaluationScore || 0), - finalGrade: calculateGrade(Number(evaluation.evaluationScore || 0)), - })) + const formData = evaluations.map(evaluation => { + // 평가점수 계산 (참고용) + const evaluationScore = calculateEvaluationScore(evaluation); + + // 최종점수가 있으면 우선 사용, 없으면 평가점수 사용 + const finalScoreValue = evaluation.finalScore + ? Number(evaluation.finalScore) + : (evaluationScore > 0 ? evaluationScore : 0); + + // 최종등급이 있으면 우선 사용, 없으면 점수 기반으로 계산 + const finalGradeValue = evaluation.finalGrade + ? evaluation.finalGrade + : calculateGrade(finalScoreValue); + + return { + id: evaluation.id, + vendorName: evaluation.vendorName || "", + vendorCode: evaluation.vendorCode || "", + evaluationScore: evaluationScore > 0 ? evaluationScore : null, + finalScore: finalScoreValue, + finalGrade: finalGradeValue, + }; + }); form.reset({ evaluations: formData }) } @@ -118,13 +164,20 @@ export function FinalizeEvaluationDialog({ // 점수 변경 시 등급 자동 계산 const handleScoreChange = (index: number, score: number) => { - const currentEvaluation = form.getValues(`evaluations.${index}`) const newGrade = calculateGrade(score) + // form.setValue를 사용하여 리렌더링 최소화 + form.setValue(`evaluations.${index}.finalGrade`, newGrade, { shouldValidate: false }) + } + + // 등급 변경 시 점수 자동 조정 + const handleGradeChange = (index: number, grade: "A" | "B" | "C" | "D") => { + const currentEvaluation = form.getValues(`evaluations.${index}`) + const newScore = calculateScoreFromGrade(grade) update(index, { ...currentEvaluation, - finalScore: score, - finalGrade: newGrade, + finalScore: newScore, + finalGrade: grade, }) } @@ -230,14 +283,26 @@ export function FinalizeEvaluationDialog({ min="0" max="100" step="0.1" - {...field} + value={field.value ?? ""} onChange={(e) => { - const value = parseFloat(e.target.value) - field.onChange(value) - if (!isNaN(value)) { - handleScoreChange(index, value) + const inputValue = e.target.value + if (inputValue === "" || inputValue === "-") { + field.onChange(0) + } else { + const numValue = Number(inputValue) + if (!isNaN(numValue)) { + // 입력 중에는 제한하지 않고, blur 시에만 제한 적용 + field.onChange(numValue) + } } }} + onBlur={(e) => { + const value = Number(e.target.value) + const clampedValue = isNaN(value) ? 0 : Math.max(0, Math.min(100, value)) + field.onChange(clampedValue) + handleScoreChange(index, clampedValue) + field.onBlur() + }} className="text-center font-mono" /> </FormControl> @@ -254,7 +319,13 @@ export function FinalizeEvaluationDialog({ render={({ field }) => ( <FormItem> <FormControl> - <Select value={field.value} onValueChange={field.onChange}> + <Select + value={field.value} + onValueChange={(value) => { + field.onChange(value as "A" | "B" | "C" | "D") + handleGradeChange(index, value as "A" | "B" | "C" | "D") + }} + > <SelectTrigger> <SelectValue /> </SelectTrigger> diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx index ae4d5dc1..fa2726b8 100644 --- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx @@ -21,7 +21,7 @@ import { DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
-import { useRouter } from "next/navigation"
+import { useRouter, useParams } from "next/navigation"
import { PQDeleteDialog } from "@/components/pq-input/pq-delete-dialog"
// PQ 제출 타입 정의
@@ -178,7 +178,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC <span className="font-medium">{row.getValue("pqNumber")}</span>
</div>
),
- excelHeader: "PQ No.",
+ meta: {
+ excelHeader: "PQ No.",
+ },
}
// 협력업체 컬럼
@@ -195,7 +197,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC ),
enableSorting: true,
enableHiding: true,
- excelHeader: "협력업체",
+ meta: {
+ excelHeader: "협력업체",
+ },
}
// PQ 유형 컬럼
@@ -223,7 +227,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC },
enableSorting: true,
enableHiding: true,
- excelHeader: "PQ 유형",
+ meta: {
+ excelHeader: "PQ 유형",
+ },
}
// 프로젝트 컬럼
@@ -251,7 +257,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC },
enableSorting: true,
enableHiding: true,
- excelHeader: "프로젝트",
+ meta: {
+ excelHeader: "프로젝트",
+ },
}
// 상태 컬럼
@@ -270,7 +278,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC },
enableSorting: false,
enableHiding: true,
- excelHeader: "진행현황",
+ meta: {
+ excelHeader: "진행현황",
+ },
};
// PQ 상태와 실사 상태를 결합하는 헬퍼 함수
@@ -416,7 +426,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC },
enableSorting: true,
enableHiding: true,
- excelHeader: "평가 결과",
+ meta: {
+ excelHeader: "평가 결과",
+ },
};
// 답변 수 컬럼
@@ -432,7 +444,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "답변 수",
+ meta: {
+ excelHeader: "답변 수",
+ },
}
const investigationAddressColumn: ExtendedColumnDef<PQSubmission> = {
@@ -453,7 +467,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "실사 주소",
+ meta: {
+ excelHeader: "실사 주소",
+ },
}
const investigationRequestedAtColumn: ExtendedColumnDef<PQSubmission> = {
accessorKey: "investigationRequestedAt",
@@ -474,7 +490,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "실사 의뢰일",
+ meta: {
+ excelHeader: "실사 의뢰일",
+ },
}
const investigationNotesColumn: ExtendedColumnDef<PQSubmission> = {
@@ -495,7 +513,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "QM 의견",
+ meta: {
+ excelHeader: "QM 의견",
+ },
}
const investigationMethodColumn: ExtendedColumnDef<PQSubmission> = {
accessorKey: "investigationMethod",
@@ -521,7 +541,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC return <span>{investigation.investigationMethod}</span>;
}
},
- excelHeader: "실사품목",
+ meta: {
+ excelHeader: "QM실사방법",
+ },
}
// 실사품목 컬럼
@@ -551,6 +573,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC );
}
},
+ meta: {
+ excelHeader: "실사품목",
+ },
}
@@ -576,7 +601,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "실사 수행 예정일",
+ meta: {
+ excelHeader: "실사 수행 예정일",
+ },
}
const investigationConfirmedAtColumn: ExtendedColumnDef<PQSubmission> = {
@@ -598,7 +625,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "실사 계획 확정일",
+ meta: {
+ excelHeader: "실사 계획 확정일",
+ },
}
const investigationCompletedAtColumn: ExtendedColumnDef<PQSubmission> = {
@@ -620,7 +649,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC </div>
)
},
- excelHeader: "실제 실사일",
+ meta: {
+ excelHeader: "실제 실사일",
+ },
}
// 제출일 컬럼
@@ -633,7 +664,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC const dateVal = row.original.createdAt as Date
return formatDate(dateVal, 'KR')
},
- excelHeader: "PQ 전송일",
+ meta: {
+ excelHeader: "PQ 전송일",
+ },
}
// 제출일 컬럼
@@ -646,7 +679,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC const dateVal = row.original.submittedAt as Date
return dateVal ? formatDate(dateVal, 'KR') : "-"
},
- excelHeader: "PQ 회신일",
+ meta: {
+ excelHeader: "PQ 회신일",
+ },
}
// 승인/거부일 컬럼
@@ -664,7 +699,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC }
return "-"
},
- excelHeader: "PQ 승인/거부일",
+ meta: {
+ excelHeader: "PQ 승인/거부일",
+ },
}
// ----------------------------------------------------------------
@@ -672,7 +709,13 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC // ----------------------------------------------------------------
const actionsColumn: ExtendedColumnDef<PQSubmission> = {
id: "actions",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="보기" />
+ ),
enableHiding: false,
+ meta: {
+ excelHeader: "보기",
+ },
cell: function Cell({ row }) {
const pq = row.original
const isSubmitted = pq.status === "SUBMITTED"
@@ -806,7 +849,9 @@ const requesterColumn: ExtendedColumnDef<PQSubmission> = { ? <span>{pqRequesterName}</span>
: <span className="text-muted-foreground">-</span>;
},
- excelHeader: "PQ/실사 요청자",
+ meta: {
+ excelHeader: "PQ/실사 요청자",
+ },
};
const qmManagerColumn: ExtendedColumnDef<PQSubmission> = {
accessorKey: "qmManager",
@@ -829,7 +874,9 @@ const qmManagerColumn: ExtendedColumnDef<PQSubmission> = { </div>
);
},
- excelHeader: "QM 담당자",
+ meta: {
+ excelHeader: "QM 담당자",
+ },
};
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 2c1aa2ca..461a635a 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -3,7 +3,7 @@ import { revalidatePath, unstable_cache, unstable_noStore } from "next/cache"; import db from "@/db/db"; -import { avlVendorInfo, paymentTerms, incoterms, rfqLastVendorQuotationItems, rfqLastVendorAttachments, rfqLastVendorResponses, RfqsLastView, rfqLastAttachmentRevisions, rfqLastAttachments, rfqsLast, rfqsLastView, users, rfqPrItems, prItemsLastView, vendors, rfqLastDetails, rfqLastVendorResponseHistory, rfqLastDetailsView, vendorContacts, projects, basicContract, basicContractTemplates, rfqLastTbeSessions, rfqLastTbeDocumentReviews, templateDetailView, RfqStatus } from "@/db/schema"; +import { avlVendorInfo, paymentTerms, incoterms, rfqLastVendorQuotationItems, rfqLastVendorAttachments, rfqLastVendorResponses, RfqsLastView, rfqLastAttachmentRevisions, rfqLastAttachments, rfqsLast, rfqsLastView, users, rfqPrItems, prItemsLastView, vendors, rfqLastDetails, rfqLastVendorResponseHistory, rfqLastDetailsView, vendorContacts, projects, basicContract, basicContractTemplates, rfqLastTbeSessions, rfqLastTbeDocumentReviews, templateDetailView, RfqStatus, purchaseRequests } from "@/db/schema"; import { sql, and, desc, asc, like, ilike, or, eq, SQL, count, gte, lte, isNotNull, ne, inArray } from "drizzle-orm"; import { filterColumns } from "@/lib/filter-columns"; import { GetRfqLastAttachmentsSchema, GetRfqsSchema } from "./validations"; @@ -135,9 +135,25 @@ export async function getRfqs(input: GetRfqsSchema) { console.log("총 데이터 수:", total); - // 6. 정렬 및 페이징 처리 - const orderByColumns = input.sort.map((sort) => { + // 6. 정렬 및 페이징 처리 (NULL 값 처리 포함) + // classNo는 별도로 처리하므로 제외 + const validSorts = input.sort.filter((sort) => sort.id !== 'classNo'); + + const orderByColumns = validSorts.map((sort) => { const column = sort.id as keyof typeof rfqsLastView.$inferSelect; + + // NULL 값이 있을 수 있는 컬럼들 (NULLS LAST 처리) + const nullableColumns = ['rfqTitle', 'majorItemMaterialDescription', 'itemCode', 'projectName', 'packageName']; + + if (nullableColumns.includes(sort.id)) { + // NULL 값은 마지막에 정렬 (NULLS LAST) + // drizzle에서 컬럼을 직접 참조하여 SQL 템플릿 사용 + const colRef = rfqsLastView[column]; + return sort.desc + ? sql`${colRef} DESC NULLS LAST` + : sql`${colRef} ASC NULLS LAST`; + } + return sort.desc ? desc(rfqsLastView[column]) : asc(rfqsLastView[column]); @@ -147,13 +163,59 @@ export async function getRfqs(input: GetRfqsSchema) { orderByColumns.push(desc(rfqsLastView.createdAt)); } - const rfqData = await db - .select() - .from(rfqsLastView) - .where(finalWhere) - .orderBy(...orderByColumns) - .limit(input.perPage) - .offset(offset); + // classNo 정렬이 있는 경우 서브쿼리로 purchaseRequests에서 classNo 가져오기 + const hasClassNoSort = input.sort.some(s => s.id === 'classNo'); + + let rfqData; + try { + if (hasClassNoSort) { + // classNo 정렬이 있는 경우 서브쿼리로 첫 번째 purchaseRequests의 classNo 가져오기 + const classNoOrderBy = input.sort + .filter(s => s.id === 'classNo') + .map(s => { + // 서브쿼리로 purchaseRequests에서 첫 번째 classNo 가져오기 (NULLS LAST 처리) + // 외부 쿼리의 rfqs_last_view.id를 참조하기 위해 correlation 사용 + return s.desc + ? sql<string | null>`(SELECT class_no FROM purchase_requests WHERE rfq_id = rfqs_last_view.id ORDER BY id LIMIT 1) DESC NULLS LAST` + : sql<string | null>`(SELECT class_no FROM purchase_requests WHERE rfq_id = rfqs_last_view.id ORDER BY id LIMIT 1) ASC NULLS LAST`; + }); + + // classNo 정렬을 먼저 적용하고 나머지 정렬을 추가 + const finalOrderBy = [...classNoOrderBy, ...orderByColumns]; + + console.log('=== classNo 정렬 실행 (서브쿼리) ===', { + hasClassNoSort, + classNoOrderByCount: classNoOrderBy.length, + finalOrderByCount: finalOrderBy.length + }); + + rfqData = await db + .select() + .from(rfqsLastView) + .where(finalWhere) + .orderBy(...finalOrderBy) + .limit(input.perPage) + .offset(offset); + } else { + rfqData = await db + .select() + .from(rfqsLastView) + .where(finalWhere) + .orderBy(...orderByColumns) + .limit(input.perPage) + .offset(offset); + } + } catch (orderError) { + console.error('정렬 오류:', orderError); + // 정렬 오류 발생 시 기본 정렬로 대체 + rfqData = await db + .select() + .from(rfqsLastView) + .where(finalWhere) + .orderBy(desc(rfqsLastView.createdAt)) + .limit(input.perPage) + .offset(offset); + } const pageCount = Math.ceil(total / input.perPage); diff --git a/lib/rfq-last/table/rfq-table.tsx b/lib/rfq-last/table/rfq-table.tsx index 9f78f578..162dd343 100644 --- a/lib/rfq-last/table/rfq-table.tsx +++ b/lib/rfq-last/table/rfq-table.tsx @@ -215,6 +215,9 @@ export function RfqTable({ expandedRows: [] }), [getSearchParam, parseSearchParam]); + // 탭별로 독립적인 tableId 사용 (정렬 상태 분리) + const tableId = React.useMemo(() => `rfq-table-${rfqCategory}`, [rfqCategory]); + const { presets, activePresetId, @@ -227,7 +230,7 @@ export function RfqTable({ setDefaultPreset, renamePreset, getCurrentSettings, - } = useTablePresets<RfqsLastView>('rfq-table', initialSettings); + } = useTablePresets<RfqsLastView>(tableId, initialSettings); // 컬럼 정의 const columns = React.useMemo(() => { @@ -305,11 +308,25 @@ export function RfqTable({ const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]); - const initialState = React.useMemo(() => ({ - sorting: initialSettings.sort.filter((s: any) => columns.some((c: any) => ("accessorKey" in c ? c.accessorKey : c.id) === s.id)), - columnVisibility: currentSettings.columnVisibility, - columnPinning: currentSettings.pinnedColumns, - }), [columns, currentSettings, initialSettings.sort]); + // 탭별로 독립적인 정렬 상태 관리 + // rfqCategory가 변경되면 정렬 상태를 재계산하여 탭 간 정렬 충돌 방지 + const initialState = React.useMemo(() => { + // 현재 탭의 컬럼에 존재하는 정렬만 유효한 것으로 필터링 + const validSorting = initialSettings.sort.filter((s: any) => + columns.some((c: any) => ("accessorKey" in c ? c.accessorKey : c.id) === s.id) + ); + + // 유효한 정렬이 없으면 기본 정렬 사용 + const sorting = validSorting.length > 0 + ? validSorting + : [{ id: "createdAt", desc: true }]; + + return { + sorting, + columnVisibility: currentSettings.columnVisibility, + columnPinning: currentSettings.pinnedColumns, + }; + }, [columns, currentSettings, initialSettings.sort, rfqCategory]); const { table } = useDataTable({ data: tableData.data, diff --git a/lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx b/lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx index 685530e6..45eea880 100644 --- a/lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx +++ b/lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx @@ -330,6 +330,19 @@ export function GeneralEvaluationFormSheet({ const progress = getProgress() + // 응답 ID -> 전역 인덱스 매핑 (카테고리 그룹/정렬과 무관하게 안정적인 인덱스 사용) + const responseIndexById = React.useMemo(() => { + const map: Record<number, number> = {} + if (formData) { + formData.evaluations.forEach((ev, idx) => { + if (ev.response?.id) { + map[ev.response.id] = idx + } + }) + } + return map + }, [formData]) + if (isLoading) { return ( <Sheet open={open} onOpenChange={onOpenChange}> @@ -430,8 +443,11 @@ export function GeneralEvaluationFormSheet({ type="button" variant="outline" size="sm" - onClick={() => handleSaveResponse(index)} - disabled={!item.response?.id} + onClick={() => { + const gi = item.response?.id ? responseIndexById[item.response.id] : undefined + return gi !== undefined ? handleSaveResponse(gi) : undefined + }} + disabled={!item.response?.id || (item.response?.id ? responseIndexById[item.response.id] === undefined : true)} > <SaveIcon className="h-4 w-4 mr-1" /> 저장 @@ -453,23 +469,41 @@ export function GeneralEvaluationFormSheet({ </CardHeader> <CardContent className="space-y-4"> {/* 📝 응답 텍스트만 (점수 입력 제거) */} - <FormField - control={form.control} - name={`responses.${index}.responseText`} - render={({ field }) => ( - <FormItem> - <FormLabel>응답 내용 *</FormLabel> - <FormControl> + {(() => { + const gi = item.response?.id ? responseIndexById[item.response.id] : undefined + if (gi === undefined) { + return ( + <div> + <FormLabel>응답 내용 *</FormLabel> <Textarea - {...field} + value={item.response?.responseText || ''} placeholder="평가 항목에 대한 응답을 상세히 작성해주세요..." className="min-h-[120px]" + disabled /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + </div> + ) + } + return ( + <FormField + control={form.control} + name={`responses.${gi}.responseText`} + render={({ field }) => ( + <FormItem> + <FormLabel>응답 내용 *</FormLabel> + <FormControl> + <Textarea + {...field} + placeholder="평가 항목에 대한 응답을 상세히 작성해주세요..." + className="min-h-[120px]" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + ) + })()} {/* 📎 첨부파일 영역 */} <div className="space-y-3"> @@ -477,7 +511,11 @@ export function GeneralEvaluationFormSheet({ <FormLabel>첨부파일</FormLabel> <div> <Input - ref={(el) => item.response?.id && (fileInputRefs.current[item.response.id] = el)} + ref={(el) => { + if (item.response?.id) { + fileInputRefs.current[item.response.id] = el + } + }} type="file" multiple className="hidden" |
