summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-04 07:48:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-04 07:48:00 +0000
commit680da9b323db8b8d7cf27c674ab0016ec87bfe81 (patch)
tree52791f8618c0c5767c6420404ddf849ae28082e8
parent153502b67da990c92973f1f8af416f9a81ec3abb (diff)
(임수민) 구매 점검 테스트 요청사항 수정 (11/02)
-rw-r--r--lib/evaluation/table/evaluation-columns.tsx54
-rw-r--r--lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx109
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-columns.tsx87
-rw-r--r--lib/rfq-last/service.ts82
-rw-r--r--lib/rfq-last/table/rfq-table.tsx29
-rw-r--r--lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx70
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"