summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-04 00:21:05 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-04 00:21:05 +0000
commitb9a109073d11262dd7ed84e25ff3cd0144c0c391 (patch)
treebe716b1681997a489972bf9561f1c8a1298e4484
parent766f95945a7ca0fdb258d6a83229593e4fcccfa6 (diff)
(최겸) 0703 평가기준표 대표님 작업사항
-rw-r--r--db/schema/evaluationCriteria.ts31
-rw-r--r--lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts2
-rw-r--r--lib/evaluation-criteria/service.ts72
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx221
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx226
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx12
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-table.tsx83
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx574
-rw-r--r--lib/evaluation-criteria/validations.ts13
9 files changed, 838 insertions, 396 deletions
diff --git a/db/schema/evaluationCriteria.ts b/db/schema/evaluationCriteria.ts
index f87b2219..9e1bd9d2 100644
--- a/db/schema/evaluationCriteria.ts
+++ b/db/schema/evaluationCriteria.ts
@@ -7,7 +7,7 @@ import {
serial,
text,
timestamp,
- varchar
+ varchar, pgEnum
} from 'drizzle-orm/pg-core';
import { eq, relations, sql } from 'drizzle-orm';
import { users } from './users';
@@ -24,10 +24,10 @@ const REG_EVAL_CRITERIA_CATEGORY = [
{ label: '품질', value: 'quality' },
];
const REG_EVAL_CRITERIA_ITEM = [
- { label: '가점항목', value: 'customer-service' },
+ { label: '가점항목', value: 'bonus' },
{ label: '납기', value: 'delivery' },
- { label: '경영현황', value: 'management-status' },
- { label: '감점항목', value: 'penalty-item' },
+ { label: '경영현황', value: 'management' },
+ { label: '감점항목', value: 'penalty' },
{ label: '구매', value: 'procurement' },
{ label: '품질', value: 'quality' },
];
@@ -44,14 +44,18 @@ const REG_EVAL_CRITERIA_ITEM_ENUM = REG_EVAL_CRITERIA_ITEM.map(c => c.value) as
// ----------------------------------------------------------------------------------------------------
+
+export const scoreTypeEnum = pgEnum("score_type", ["fixed", "variable"]);
+
+
/* TABLE SCHEMATA */
const regEvalCriteria = pgTable('reg_eval_criteria', {
id: serial('id').primaryKey(),
- category: varchar('category', { enum: REG_EVAL_CRITERIA_CATEGORY_ENUM, length: 32 }).default('quality').notNull(),
- category2: varchar('category2', { enum: REG_EVAL_CRITERIA_CATEGORY2_ENUM, length: 32 }).default('processScore').notNull(),
- item: varchar('item', { enum: REG_EVAL_CRITERIA_ITEM_ENUM, length: 32 }).default('quality').notNull(),
- classification: varchar('classification', { length: 255 }).notNull(),
- range: varchar('range', { length: 255 }),
+ category: varchar('category', { enum: REG_EVAL_CRITERIA_CATEGORY_ENUM, length: 32 }).default('quality').notNull(), //평가부문
+ category2: varchar('category2', { enum: REG_EVAL_CRITERIA_CATEGORY2_ENUM, length: 32 }).default('processScore').notNull(), //점수유형
+ item: varchar('item', { enum: REG_EVAL_CRITERIA_ITEM_ENUM, length: 32 }).default('quality').notNull(), //항목
+ classification: varchar('classification', { length: 255 }).notNull(),//구분
+ range: varchar('range', { length: 255 }),//범위, 실제로 평가명
remarks: text('remarks'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
@@ -63,7 +67,14 @@ const regEvalCriteria = pgTable('reg_eval_criteria', {
.notNull()
.references(() => users.id, { onDelete: "set null" }),
+ //새로 추가
+ variableScoreMin: decimal('variable_score_min ', { precision: 5, scale: 2 }),
+ variableScoreMax: decimal('variable_score_max ', { precision: 5, scale: 2 }),
+ variableScoreUnit: varchar('variable_score_unit ', { length: 100 }),
+ scoreType: scoreTypeEnum("score_type").notNull().default("fixed"),
});
+
+
const regEvalCriteriaDetails = pgTable('reg_eval_criteria_details', {
id: serial('id').primaryKey(),
criteriaId: integer('criteria_id')
@@ -88,7 +99,7 @@ const regEvalCriteriaView = pgView('reg_eval_criteria_view').as((qb) =>
id: regEvalCriteriaDetails.id,
criteriaId: regEvalCriteriaDetails.criteriaId,
category: regEvalCriteria.category,
- scoreCategory: regEvalCriteria.category2,
+ category2: regEvalCriteria.category2,
item: regEvalCriteria.item,
classification: regEvalCriteria.classification,
range: regEvalCriteria.range,
diff --git a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts b/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts
index eb3d7020..92c7a25e 100644
--- a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts
+++ b/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts
@@ -318,7 +318,7 @@ export async function importRegEvalCriteriaExcel(file: File): Promise<ImportResu
const matchedExistingCriteria = matchedExistingDetails[0];
const criteriaChanged = (
matchedExistingCriteria.category !== criteriaData.category ||
- matchedExistingCriteria.scoreCategory !== criteriaData.category2 ||
+ matchedExistingCriteria.category2 !== criteriaData.category2 ||
matchedExistingCriteria.item !== criteriaData.item ||
matchedExistingCriteria.classification !== criteriaData.classification ||
matchedExistingCriteria.range !== criteriaData.range ||
diff --git a/lib/evaluation-criteria/service.ts b/lib/evaluation-criteria/service.ts
index 8badc61f..e204579f 100644
--- a/lib/evaluation-criteria/service.ts
+++ b/lib/evaluation-criteria/service.ts
@@ -8,7 +8,7 @@ import {
asc,
desc,
ilike,
- or,
+ or,count, eq
} from 'drizzle-orm';
import {
countRegEvalCriteria,
@@ -27,7 +27,7 @@ import {
REG_EVAL_CRITERIA_CATEGORY2_ENUM,
REG_EVAL_CRITERIA_CATEGORY_ENUM,
REG_EVAL_CRITERIA_ITEM_ENUM,
- regEvalCriteriaView,
+ regEvalCriteria, regEvalCriteriaDetails, // regEvalCriteriaView 대신 regEvalCriteria 사용
type NewRegEvalCriteria,
type NewRegEvalCriteriaDetails,
type RegEvalCriteria,
@@ -42,24 +42,25 @@ import { type GetRegEvalCriteriaSchema } from './validations';
// ----------------------------------------------------------------------------------------------------
-/* FUNCTION FOR GETTING CRITERIA */
+/* FUNCTION FOR GETTING CRITERIA - 메인 기준 목록만 가져오기 */
async function getRegEvalCriteria(input: GetRegEvalCriteriaSchema) {
try {
const offset = (input.page - 1) * input.perPage;
const advancedWhere = filterColumns({
- table: regEvalCriteriaView,
+ table: regEvalCriteria, // view 대신 메인 테이블 사용
filters: input.filters,
joinOperator: input.joinOperator,
});
- // Filtering
+ // Filtering - 메인 테이블 컬럼들 기준으로 검색
let globalWhere;
if (input.search) {
const s = `%${input.search}%`;
globalWhere = or(
- ilike(regEvalCriteriaView.category, s),
- ilike(regEvalCriteriaView.item, s),
- ilike(regEvalCriteriaView.classification, s),
+ ilike(regEvalCriteria.category, s),
+ ilike(regEvalCriteria.item, s),
+ ilike(regEvalCriteria.classification, s),
+ ilike(regEvalCriteria.range, s), // range 검색 추가
);
}
const finalWhere = and(advancedWhere, globalWhere);
@@ -68,20 +69,27 @@ async function getRegEvalCriteria(input: GetRegEvalCriteriaSchema) {
const orderBy = input.sort.length > 0
? input.sort.map((item) => {
return item.desc
- ? desc(regEvalCriteriaView[item.id])
- : asc(regEvalCriteriaView[item.id]);
+ ? desc(regEvalCriteria[item.id])
+ : asc(regEvalCriteria[item.id]);
})
- : [asc(regEvalCriteriaView.id)];
+ : [asc(regEvalCriteria.id)];
- // Getting Data
+ // Getting Data - 메인 기준 데이터만 가져오기
const { data, total } = await db.transaction(async (tx) => {
- const data = await selectRegEvalCriteria(tx, {
- where: finalWhere,
- orderBy,
- offset,
- limit: input.perPage,
- });
- const total = await countRegEvalCriteria(tx, finalWhere);
+ const data = await tx
+ .select()
+ .from(regEvalCriteria)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(input.perPage);
+
+ const totalResult = await tx
+ .select({ count: count() })
+ .from(regEvalCriteria)
+ .where(finalWhere);
+
+ const total = totalResult[0]?.count || 0;
return { data, total };
});
@@ -105,6 +113,26 @@ async function getRegEvalCriteriaWithDetails(id: number) {
}
}
+/* FUNCTION FOR GETTING CRITERIA DETAILS ONLY - 특정 기준의 상세 항목들만 가져오기 */
+async function getRegEvalCriteriaDetails(criteriaId: number) {
+ try {
+
+ console.log(criteriaId,"criteriaId")
+ return await db.transaction(async (tx) => {
+ const details = await tx
+ .select()
+ .from(regEvalCriteriaDetails)
+ .where(eq(regEvalCriteriaDetails.criteriaId, criteriaId))
+ .orderBy(asc(regEvalCriteriaDetails.orderIndex));
+
+ return details;
+ });
+ } catch (err) {
+ console.error('Error in Getting Regular Evaluation Criteria Details: ', err);
+ return [];
+ }
+}
+
// ----------------------------------------------------------------------------------------------------
/* FUNCTION FOR CREATING CRITERIA WITH DETAILS */
@@ -145,6 +173,9 @@ async function modifyRegEvalCriteriaWithDetails(
) {
try {
return await db.transaction(async (tx) => {
+
+ console.log(id, criteriaData)
+
const modifiedCriteria = await updateRegEvalCriteria(tx, id, criteriaData);
const originCriteria = await getRegEvalCriteriaWithDetails(id);
const originCriteriaDetails = originCriteria?.criteriaDetails || [];
@@ -214,8 +245,6 @@ async function removeRegEvalCriteriaDetails(id: number) {
}
}
-
-
// ----------------------------------------------------------------------------------------------------
/* EXPORT */
@@ -224,6 +253,7 @@ export {
modifyRegEvalCriteriaWithDetails,
getRegEvalCriteria,
getRegEvalCriteriaWithDetails,
+ getRegEvalCriteriaDetails, // 새로 추가
removeRegEvalCriteria,
removeRegEvalCriteriaDetails,
}; \ No newline at end of file
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx
index bdf583bc..88c8107b 100644
--- a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx
+++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx
@@ -10,14 +10,16 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuShortcut,
+ DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { Ellipsis } from 'lucide-react';
+import { Ellipsis, Eye, Edit, Trash2 } from 'lucide-react';
import {
REG_EVAL_CRITERIA_CATEGORY,
REG_EVAL_CRITERIA_CATEGORY2,
REG_EVAL_CRITERIA_ITEM,
- type RegEvalCriteriaView,
+ type RegEvalCriteria, // RegEvalCriteriaView 대신 RegEvalCriteria 사용
} from '@/db/schema';
import { type ColumnDef } from '@tanstack/react-table';
import { type DataTableRowAction } from '@/types/table';
@@ -26,16 +28,16 @@ import { type DataTableRowAction } from '@/types/table';
/* TYPES */
interface GetColumnsProps {
- setRowAction: Dispatch<SetStateAction<DataTableRowAction<RegEvalCriteriaView> | null>>,
+ setRowAction: Dispatch<SetStateAction<DataTableRowAction<RegEvalCriteria> | null>>,
};
// ----------------------------------------------------------------------------------------------------
/* FUNCTION FOR GETTING COLUMNS SETTING */
-function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteriaView>[] {
+function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteria>[] {
// [1] SELECT COLUMN - CHECKBOX
- const selectColumn: ColumnDef<RegEvalCriteriaView> = {
+ const selectColumn: ColumnDef<RegEvalCriteria> = {
id: 'select',
header: ({ table }) => (
<Checkbox
@@ -58,11 +60,11 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri
),
enableSorting: false,
enableHiding: false,
- size:40,
+ size: 40,
};
// [2] CRITERIA COLUMNS
- const criteriaColumns: ColumnDef<RegEvalCriteriaView>[] = [
+ const criteriaColumns: ColumnDef<RegEvalCriteria>[] = [
{
accessorKey: 'category',
header: ({ column }) => (
@@ -146,149 +148,129 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri
{
accessorKey: 'range',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="범위" />
+ <DataTableColumnHeaderSimple column={column} title="평가명" />
),
cell: ({ row }) => (
- <div className="font-regular">
+ <div className="font-medium">
{row.getValue('range') || '-'}
</div>
),
enableSorting: true,
enableHiding: false,
meta: {
- excelHeader: 'Range',
+ excelHeader: 'Evaluation Name',
type: 'text',
},
},
{
- accessorKey: 'detail',
+ accessorKey: 'scoreType',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="평가내용" />
- ),
- cell: ({ row }) => (
- <div className="font-bold">
- {row.getValue('detail')}
- </div>
- ),
- enableSorting: true,
- enableHiding: false,
- meta: {
- excelHeader: 'Detail',
- type: 'text',
- },
- },
- ];
-
- // [3] SCORE COLUMNS
- const scoreEquipColumns: ColumnDef<RegEvalCriteriaView>[] = [
- {
- accessorKey: 'scoreEquipShip',
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="조선" />
+ <DataTableColumnHeaderSimple column={column} title="점수유형" />
),
cell: ({ row }) => {
- const value = row.getValue<string>('scoreEquipShip');
- const displayValue = typeof value === 'string'
- ? parseFloat(parseFloat(value).toFixed(2)).toString()
- : '-';
+ const value = row.getValue<string>('scoreType');
return (
- <div className="font-bold">
- {displayValue}
- </div>
+ <Badge variant={value === 'fixed' ? 'default' : 'secondary'}>
+ {value === 'fixed' ? '고정점수' : '변동점수'}
+ </Badge>
);
},
enableSorting: true,
enableHiding: false,
meta: {
- excelHeader: 'Equipment-Shipbuilding Score',
- group: 'Equipment Score',
- type: 'number',
+ excelHeader: 'Score Type',
+ type: 'select',
},
},
+ ];
+
+ // [3] VARIABLE SCORE COLUMNS (변동점수 관련 컬럼들)
+ const variableScoreColumns: ColumnDef<RegEvalCriteria>[] = [
{
- accessorKey: 'scoreEquipMarine',
+ accessorKey: 'variableScoreMin',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="해양" />
+ <DataTableColumnHeaderSimple column={column} title="최소점수" />
),
cell: ({ row }) => {
- const value = row.getValue<string>('scoreEquipMarine');
+ const value = row.getValue<string>('variableScoreMin');
+ const scoreType = row.getValue<string>('scoreType');
+ if (scoreType !== 'variable') return <div className="text-gray-400">-</div>;
+
const displayValue = typeof value === 'string'
? parseFloat(parseFloat(value).toFixed(2)).toString()
: '-';
return (
- <div className="font-bold">
+ <div className="font-regular text-center">
{displayValue}
</div>
);
},
enableSorting: true,
- enableHiding: false,
+ enableHiding: true,
meta: {
- excelHeader: 'Equipment-Marine Engineering Score',
- group: 'Equipment Score',
+ excelHeader: 'Min Score',
type: 'number',
},
},
- ];
- const scoreBulkColumns: ColumnDef<RegEvalCriteriaView>[] = [
{
- accessorKey: 'scoreBulkShip',
+ accessorKey: 'variableScoreMax',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="조선" />
+ <DataTableColumnHeaderSimple column={column} title="최대점수" />
),
cell: ({ row }) => {
- const value = row.getValue<string>('scoreBulkShip');
+ const value = row.getValue<string>('variableScoreMax');
+ const scoreType = row.getValue<string>('scoreType');
+ if (scoreType !== 'variable') return <div className="text-gray-400">-</div>;
+
const displayValue = typeof value === 'string'
? parseFloat(parseFloat(value).toFixed(2)).toString()
: '-';
return (
- <div className="font-bold">
+ <div className="font-regular text-center">
{displayValue}
</div>
);
},
enableSorting: true,
- enableHiding: false,
+ enableHiding: true,
meta: {
- excelHeader: 'Bulk-Shipbuilding Score',
- group: 'Bulk Score',
+ excelHeader: 'Max Score',
type: 'number',
},
},
{
- accessorKey: 'scoreBulkMarine',
+ accessorKey: 'variableScoreUnit',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="해양" />
+ <DataTableColumnHeaderSimple column={column} title="점수단위" />
),
cell: ({ row }) => {
- const value = row.getValue<string>('scoreBulkMarine');
- const displayValue = typeof value === 'string'
- ? parseFloat(parseFloat(value).toFixed(2)).toString()
- : '-';
+ const value = row.getValue<string>('variableScoreUnit');
+ const scoreType = row.getValue<string>('scoreType');
+ if (scoreType !== 'variable') return <div className="text-gray-400">-</div>;
+
return (
- <div className="font-bold">
- {displayValue}
+ <div className="font-regular text-center">
+ {value || '-'}
</div>
);
},
enableSorting: true,
- enableHiding: false,
+ enableHiding: true,
meta: {
- excelHeader: 'Bulk-Marine Engineering Score',
- group: 'Bulk Score',
- type: 'number',
+ excelHeader: 'Score Unit',
+ type: 'text',
},
},
];
// [4] REMARKS COLUMN
- const remarksColumn: ColumnDef<RegEvalCriteriaView> = {
+ const remarksColumn: ColumnDef<RegEvalCriteria> = {
accessorKey: 'remarks',
header: ({ column }) => (
<DataTableColumnHeaderSimple column={column} title="비고" />
),
cell: ({ row }) => (
- <div className="font-regular">
+ <div className="font-regular max-w-[150px] truncate" title={row.getValue('remarks')}>
{row.getValue('remarks') || '-'}
</div>
),
@@ -300,8 +282,8 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri
},
};
- // [5] HIDDEN ID COLUMN
- const hiddenColumns: ColumnDef<RegEvalCriteriaView>[] = [
+ // [5] HIDDEN ID COLUMNS
+ const hiddenColumns: ColumnDef<RegEvalCriteria>[] = [
{
accessorKey: 'id',
header: ({ column }) => (
@@ -321,66 +303,86 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri
},
},
{
- accessorKey: 'criteriaId',
+ accessorKey: 'createdAt',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="기준 ID" />
- ),
- cell: ({ row }) => (
- <div className="font-regular">
- {row.getValue('criteriaId')}
- </div>
+ <DataTableColumnHeaderSimple column={column} title="생성일시" />
),
+ cell: ({ row }) => {
+ const date = row.getValue<Date>('createdAt');
+ return (
+ <div className="font-regular">
+ {date ? new Date(date).toLocaleDateString('ko-KR') : '-'}
+ </div>
+ );
+ },
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: 'Criteria ID',
+ excelHeader: 'Created At',
group: 'Meta Data',
- type: 'criteriaId',
+ type: 'date',
},
},
{
- accessorKey: 'orderIndex',
+ accessorKey: 'updatedAt',
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="정렬 순서" />
- ),
- cell: ({ row }) => (
- <div className="font-regular">
- {row.getValue('orderIndex')}
- </div>
+ <DataTableColumnHeaderSimple column={column} title="수정일시" />
),
+ cell: ({ row }) => {
+ const date = row.getValue<Date>('updatedAt');
+ return (
+ <div className="font-regular">
+ {date ? new Date(date).toLocaleDateString('ko-KR') : '-'}
+ </div>
+ );
+ },
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: 'Order Index',
+ excelHeader: 'Updated At',
group: 'Meta Data',
- type: 'number',
+ type: 'date',
},
},
];
- // [6] ACTIONS COLUMN - DROPDOWN MENU
- const actionsColumn: ColumnDef<RegEvalCriteriaView> = {
+ // [6] ACTIONS COLUMN - DROPDOWN MENU WITH VIEW ACTION
+ const actionsColumn: ColumnDef<RegEvalCriteria> = {
id: 'actions',
enableHiding: false,
cell: function Cell({ row }) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Button variant="ghost" size="icon">
- <Ellipsis className="size-4" aria-hidden="true" />
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
- <DropdownMenuContent align="end">
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "view" })}
+ >
+ <Eye className="mr-2 size-4" />
+ 상세보기
+ </DropdownMenuItem>
+ <DropdownMenuSeparator />
<DropdownMenuItem
- onClick={() => setRowAction({ row, type: 'update' })}
+ onSelect={() => setRowAction({ row, type: "update" })}
>
- Modify Criteria
+ <Edit className="mr-2 size-4" />
+ 수정하기
</DropdownMenuItem>
+ <DropdownMenuSeparator />
<DropdownMenuItem
- onClick={() => setRowAction({ row, type: 'delete' })}
- className="text-destructive"
+ onSelect={() => setRowAction({ row, type: "delete" })}
>
- Delete Criteria
+ <Trash2 className="mr-2 size-4" />
+ 삭제하기
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -392,18 +394,9 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri
return [
selectColumn,
...criteriaColumns,
- {
- id: 'scoreEquip',
- header: '기자재',
- columns: scoreEquipColumns,
- },
- {
- id: 'scoreBulk',
- header: '벌크',
- columns: scoreBulkColumns,
- },
- ...hiddenColumns,
+ ...variableScoreColumns,
remarksColumn,
+ ...hiddenColumns,
actionsColumn,
];
};
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx
new file mode 100644
index 00000000..60ca173b
--- /dev/null
+++ b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx
@@ -0,0 +1,226 @@
+'use client';
+
+/* IMPORT */
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Separator } from '@/components/ui/separator';
+import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { useEffect, useState } from 'react';
+import { Loader2, X } from 'lucide-react';
+import {
+ REG_EVAL_CRITERIA_CATEGORY,
+ REG_EVAL_CRITERIA_CATEGORY2,
+ REG_EVAL_CRITERIA_ITEM,
+ type RegEvalCriteriaView,
+ type RegEvalCriteriaDetails,
+} from '@/db/schema';
+import { getRegEvalCriteriaDetails } from '../service'; // 서버 액션 import
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TYPES */
+interface RegEvalCriteriaDetailsSheetProps {
+ criteriaViewData: RegEvalCriteriaView;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+// ----------------------------------------------------------------------------------------------------
+
+/* CRITERIA DETAILS SHEET COMPONENT */
+export function RegEvalCriteriaDetailsSheet({
+ criteriaViewData,
+ open,
+ onOpenChange
+}: RegEvalCriteriaDetailsSheetProps) {
+ const [details, setDetails] = useState<RegEvalCriteriaDetails[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+
+ // 상세 항목들 가져오기
+ useEffect(() => {
+ if (criteriaViewData?.id && open) {
+ setLoading(true);
+ setError(null);
+
+ getRegEvalCriteriaDetails(criteriaViewData.id)
+ .then((fetchedDetails) => {
+ setDetails(fetchedDetails || []);
+ })
+ .catch((err) => {
+ console.error('Failed to fetch criteria details:', err);
+ setError('상세 정보를 불러오는데 실패했습니다.');
+ setDetails([]);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+ }, [criteriaViewData?.id, open]);
+
+ // 라벨 변환 함수들
+ const getCategoryLabel = (value: string) =>
+ REG_EVAL_CRITERIA_CATEGORY.find(item => item.value === value)?.label ?? value;
+
+ const getCategory2Label = (value: string) =>
+ REG_EVAL_CRITERIA_CATEGORY2.find(item => item.value === value)?.label ?? value;
+
+ const getItemLabel = (value: string) =>
+ REG_EVAL_CRITERIA_ITEM.find(item => item.value === value)?.label ?? value;
+
+ // 점수 표시 함수
+ const formatScore = (score: string | null | undefined) => {
+ if (!score) return '-';
+ const numericScore = typeof score === 'string' ? parseFloat(score) : score;
+ return isNaN(numericScore) ? '-' : parseFloat(numericScore.toFixed(2)).toString();
+ };
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="w-[800px] sm:w-[900px] sm:max-w-[90vw]" style={{width:900}}>
+ <SheetHeader>
+ <SheetTitle className="flex items-center justify-between">
+ <span>평가 기준 상세보기</span>
+ </SheetTitle>
+ </SheetHeader>
+
+ <ScrollArea className="h-[calc(100vh-120px)] pr-4">
+ <div className="space-y-6 py-4">
+ {/* 기본 정보 카드 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">기본 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">평가부문</p>
+ <Badge variant="default" className="mt-1">
+ {getCategoryLabel(criteriaViewData.category)}
+ </Badge>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">점수구분</p>
+ <Badge variant="secondary" className="mt-1">
+ {getCategory2Label(criteriaViewData.category2)}
+ </Badge>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">항목</p>
+ <Badge variant="outline" className="mt-1">
+ {getItemLabel(criteriaViewData.item)}
+ </Badge>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">구분</p>
+ <p className="text-sm mt-1">{criteriaViewData.classification}</p>
+ </div>
+ </div>
+
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">평가명 (범위)</p>
+ <p className="text-sm mt-1 font-medium">{criteriaViewData.range || '-'}</p>
+ </div>
+
+ {criteriaViewData.remarks && (
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">비고</p>
+ <p className="text-sm mt-1">{criteriaViewData.remarks}</p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ <Separator />
+
+ {/* 평가 옵션 및 점수 카드 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">평가 옵션 및 점수</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 각 평가 옵션에 따른 점수를 확인할 수 있습니다.
+ </p>
+ </CardHeader>
+ <CardContent>
+ {loading ? (
+ <div className="flex justify-center items-center py-8">
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ <span className="text-sm text-muted-foreground">로딩 중...</span>
+ </div>
+ ) : error ? (
+ <div className="flex justify-center items-center py-8">
+ <div className="text-sm text-destructive">{error}</div>
+ </div>
+ ) : details.length === 0 ? (
+ <div className="flex justify-center items-center py-8">
+ <div className="text-sm text-muted-foreground">등록된 평가 옵션이 없습니다.</div>
+ </div>
+ ) : (
+ <div className="border rounded-lg">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-12">#</TableHead>
+ <TableHead className="min-w-[200px]">평가 옵션</TableHead>
+ <TableHead className="text-center w-24">기자재-조선</TableHead>
+ <TableHead className="text-center w-24">기자재-해양</TableHead>
+ <TableHead className="text-center w-24">벌크-조선</TableHead>
+ <TableHead className="text-center w-24">벌크-해양</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {details.map((detail, index) => (
+ <TableRow key={detail.id}>
+ <TableCell className="font-medium">
+ {(detail.orderIndex ?? index) + 1}
+ </TableCell>
+ <TableCell className="font-medium">
+ {detail.detail}
+ </TableCell>
+ <TableCell className="text-center">
+ <Badge variant="outline" className="font-mono">
+ {formatScore(detail.scoreEquipShip)}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-center">
+ <Badge variant="outline" className="font-mono">
+ {formatScore(detail.scoreEquipMarine)}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-center">
+ <Badge variant="outline" className="font-mono">
+ {formatScore(detail.scoreBulkShip)}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-center">
+ <Badge variant="outline" className="font-mono">
+ {formatScore(detail.scoreBulkMarine)}
+ </Badge>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ </div>
+ </ScrollArea>
+ </SheetContent>
+ </Sheet>
+ );
+}
+
+// ----------------------------------------------------------------------------------------------------
+
+/* EXPORT */
+export default RegEvalCriteriaDetailsSheet; \ No newline at end of file
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx
index d33e7d29..3362d810 100644
--- a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx
+++ b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx
@@ -60,7 +60,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
/* TYPES */
const regEvalCriteriaFormSchema = z.object({
category: z.string().min(1, '평가부문은 필수 항목입니다.'),
- scoreCategory: z.string().min(1, '점수구분은 필수 항목입니다.'),
+ category2: z.string().min(1, '점수구분은 필수 항목입니다.'),
item: z.string().min(1, '항목은 필수 항목입니다.'),
classification: z.string().min(1, '구분은 필수 항목입니다.'),
range: z.string().nullable().optional(),
@@ -245,7 +245,7 @@ function RegEvalCriteriaFormSheet({
resolver: zodResolver(regEvalCriteriaFormSchema),
defaultValues: {
category: '',
- scoreCategory: '',
+ category2: '',
item: '',
classification: '',
range: '',
@@ -276,7 +276,7 @@ function RegEvalCriteriaFormSheet({
if (targetData) {
form.reset({
category: targetData.category,
- scoreCategory: targetData.scoreCategory,
+ category2: targetData.category2,
item: targetData.item,
classification: targetData.classification,
range: targetData.range,
@@ -303,7 +303,7 @@ function RegEvalCriteriaFormSheet({
} else if (open && !isUpdateMode) {
form.reset({
category: '',
- scoreCategory: '',
+ category2: '',
item: '',
classification: '',
range: '',
@@ -327,7 +327,7 @@ function RegEvalCriteriaFormSheet({
try {
const criteriaData = {
category: data.category,
- scoreCategory: data.scoreCategory,
+ category2: data.category2,
item: data.item,
classification: data.classification,
range: data.range,
@@ -414,7 +414,7 @@ function RegEvalCriteriaFormSheet({
/>
<FormField
control={form.control}
- name="scoreCategory"
+ name="category2"
render={({ field }) => (
<FormItem>
<FormLabel>점수구분</FormLabel>
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-table.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-table.tsx
index d73eb5bd..e2d614e0 100644
--- a/lib/evaluation-criteria/table/reg-eval-criteria-table.tsx
+++ b/lib/evaluation-criteria/table/reg-eval-criteria-table.tsx
@@ -3,17 +3,18 @@
/* IMPORT */
import { DataTable } from '@/components/data-table/data-table';
import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar';
-import getColumns from './reg-eval-criteria-columns';
+import getColumns from './reg-eval-criteria-columns'; // 새로운 컬럼 파일 사용
import { getRegEvalCriteria } from '../service';
import {
REG_EVAL_CRITERIA_CATEGORY,
REG_EVAL_CRITERIA_CATEGORY2,
REG_EVAL_CRITERIA_ITEM,
- type RegEvalCriteriaView
+ type RegEvalCriteria // RegEvalCriteriaView 대신 RegEvalCriteria 사용
} from '@/db/schema';
import RegEvalCriteriaCreateDialog from './reg-eval-criteria-create-dialog';
import RegEvalCriteriaDeleteDialog from './reg-eval-criteria-delete-dialog';
import RegEvalCriteriaUpdateSheet from './reg-eval-criteria-update-sheet';
+import RegEvalCriteriaDetailsSheet from './reg-eval-criteria-details-sheet'; // 새로 추가
import RegEvalCriteriaTableToolbarActions from './reg-eval-criteria-table-toolbar-actions';
import {
type DataTableFilterField,
@@ -38,7 +39,7 @@ interface RegEvalCriteriaTableProps {
/* TABLE COMPONENT */
function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
const router = useRouter();
- const [rowAction, setRowAction] = useState<DataTableRowAction<RegEvalCriteriaView> | null>(null);
+ const [rowAction, setRowAction] = useState<DataTableRowAction<RegEvalCriteria> | null>(null);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState<boolean>(false);
const [promiseData] = use(promises);
const tableData = promiseData;
@@ -48,7 +49,7 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
[setRowAction],
);
- const filterFields: DataTableFilterField<RegEvalCriteriaView>[] = [
+ const filterFields: DataTableFilterField<RegEvalCriteria>[] = [
{
id: 'category',
label: '평가부문',
@@ -65,7 +66,7 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
placeholder: '항목 선택...',
},
]
- const advancedFilterFields: DataTableAdvancedFilterField<RegEvalCriteriaView>[] = [
+ const advancedFilterFields: DataTableAdvancedFilterField<RegEvalCriteria>[] = [
{
id: 'category',
label: '평가부문',
@@ -85,12 +86,13 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
options: REG_EVAL_CRITERIA_ITEM,
},
{ id: 'classification', label: '구분', type: 'text' },
- { id: 'range', label: '범위', type: 'text' },
- { id: 'detail', label: '평가내용', type: 'text' },
- { id: 'scoreEquipShip', label: '조선', type: 'number' },
- { id: 'scoreEquipMarine', label: '해양', type: 'number' },
- { id: 'scoreBulkShip', label: '조선', type: 'number' },
- { id: 'scoreBulkMarine', label: '해양', type: 'number' },
+ { id: 'range', label: '평가명', type: 'text' },
+ { id: 'scoreType', label: '점수유형', type: 'select',
+ options: [
+ { label: '고정점수', value: 'fixed' },
+ { label: '변동점수', value: 'variable' }
+ ]
+ },
{ id: 'remarks', label: '비고', type: 'text' },
];
@@ -104,14 +106,18 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
enableAdvancedFilter: true,
initialState: {
sorting: [
- { id: 'criteriaId', desc: false },
- { id: 'orderIndex', desc: false },
+ { id: 'id', desc: false },
],
columnPinning: { left: ['select'], right: ['actions'] },
columnVisibility: {
id: false,
- criteriaId: false,
- orderIndex: false,
+ createdAt: false,
+ updatedAt: false,
+ createdBy: false,
+ updatedBy: false,
+ variableScoreMin: false,
+ variableScoreMax: false,
+ variableScoreUnit: false,
},
},
getRowId: (originalRow) => String(originalRow.id),
@@ -119,42 +125,52 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
clearOnDefault: true,
});
- const emptyCriteriaViewData: RegEvalCriteriaView = {
- id: null,
+ const emptyCriteriaData: RegEvalCriteria = {
+ id: 0,
category: '',
category2: '',
item: '',
classification: '',
range: null,
remarks: null,
- criteriaId: null,
- detail: '',
- orderIndex: null,
- scoreEquipShip: null,
- scoreEquipMarine: null,
- scoreBulkShip: null,
- scoreBulkMarine: null,
+ scoreType: 'fixed',
+ variableScoreMin: null,
+ variableScoreMax: null,
+ variableScoreUnit: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ createdBy: 0,
+ updatedBy: 0,
};
const refreshData = useCallback(() => {
router.refresh();
}, [router]);
+
const handleCreateCriteria = () => {
setIsCreateDialogOpen(true);
};
+
const handleCreateSuccess = useCallback(() => {
setIsCreateDialogOpen(false);
refreshData();
}, [refreshData]);
+
const handleModifySuccess = useCallback(() => {
setRowAction(null);
refreshData();
}, [refreshData]);
+
const handleDeleteSuccess = useCallback(() => {
setRowAction(null);
refreshData();
}, [refreshData]);
+ // 상세보기 핸들러 추가
+ const handleDetailsClose = useCallback(() => {
+ setRowAction(null);
+ }, []);
+
return (
<>
<DataTable table={table}>
@@ -170,21 +186,34 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
/>
</DataTableAdvancedToolbar>
</DataTable>
+
+ {/* 생성 다이얼로그 */}
<RegEvalCriteriaCreateDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
onSuccess={handleCreateSuccess}
/>
+
+ {/* 상세보기 시트 - 새로 추가 */}
+ <RegEvalCriteriaDetailsSheet
+ open={rowAction?.type === 'view'}
+ onOpenChange={handleDetailsClose}
+ criteriaViewData={rowAction?.row.original ?? emptyCriteriaData}
+ />
+
+ {/* 수정 시트 */}
<RegEvalCriteriaUpdateSheet
open={rowAction?.type === 'update'}
onOpenChange={() => setRowAction(null)}
- criteriaViewData={rowAction?.row.original ?? emptyCriteriaViewData}
+ criteriaData={rowAction?.row.original ?? emptyCriteriaData}
onSuccess={handleModifySuccess}
/>
+
+ {/* 삭제 다이얼로그 */}
<RegEvalCriteriaDeleteDialog
open={rowAction?.type === 'delete'}
onOpenChange={() => setRowAction(null)}
- criteriaViewData={rowAction?.row.original ?? emptyCriteriaViewData}
+ criteriaViewData={rowAction?.row.original ?? emptyCriteriaData}
onSuccess={handleDeleteSuccess}
/>
</>
@@ -194,4 +223,4 @@ function RegEvalCriteriaTable({ promises }: RegEvalCriteriaTableProps) {
// ----------------------------------------------------------------------------------------------------
/* EXPORT */
-export default RegEvalCriteriaTable;
+export default RegEvalCriteriaTable; \ No newline at end of file
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx
index 7f40b318..bbf4f36d 100644
--- a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx
+++ b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx
@@ -30,9 +30,8 @@ import {
REG_EVAL_CRITERIA_CATEGORY2,
REG_EVAL_CRITERIA_ITEM,
type RegEvalCriteriaDetails,
- type RegEvalCriteriaView,
+ type RegEvalCriteria, // RegEvalCriteriaView 대신 RegEvalCriteria 사용
} from '@/db/schema';
-import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
@@ -47,6 +46,7 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
+import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { toast } from 'sonner';
import { useForm, useFieldArray } from 'react-hook-form';
@@ -64,6 +64,11 @@ const regEvalCriteriaFormSchema = z.object({
classification: z.string().min(1, '구분은 필수 항목입니다.'),
range: z.string().nullable().optional(),
remarks: z.string().nullable().optional(),
+ // 새로운 필드들 추가
+ scoreType: z.enum(['fixed', 'variable']).default('fixed'),
+ variableScoreMin: z.coerce.number().nullable().optional(),
+ variableScoreMax: z.coerce.number().nullable().optional(),
+ variableScoreUnit: z.string().nullable().optional(),
criteriaDetails: z.array(
z.object({
id: z.number().optional(),
@@ -75,18 +80,22 @@ const regEvalCriteriaFormSchema = z.object({
})
).min(1, '최소 1개의 평가 내용이 필요합니다.'),
});
+
type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>;
+
interface CriteriaDetailFormProps {
index: number
form: any
onRemove: () => void
canRemove: boolean
disabled?: boolean
+ scoreType: 'fixed' | 'variable'
}
+
interface RegEvalCriteriaUpdateSheetProps {
open: boolean,
onOpenChange: (open: boolean) => void,
- criteriaViewData: RegEvalCriteriaView,
+ criteriaData: RegEvalCriteria, // criteriaViewData → criteriaData로 변경
onSuccess: () => void,
};
@@ -99,13 +108,14 @@ function CriteriaDetailForm({
onRemove,
canRemove,
disabled = false,
+ scoreType,
}: CriteriaDetailFormProps) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
- <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle>
+ <CardTitle className="text-lg">평가 옵션 {index + 1}</CardTitle>
{canRemove && (
<Button
type="button"
@@ -133,10 +143,10 @@ function CriteriaDetailForm({
name={`criteriaDetails.${index}.detail`}
render={({ field }) => (
<FormItem>
- <FormLabel>평가내용</FormLabel>
+ <FormLabel>평가 옵션 내용</FormLabel>
<FormControl>
<Textarea
- placeholder="평가내용을 입력하세요."
+ placeholder="평가 옵션 내용을 입력하세요. (예: 우수, 보통, 미흡)"
{...field}
disabled={disabled}
/>
@@ -145,86 +155,102 @@ function CriteriaDetailForm({
</FormItem>
)}
/>
- <FormField
- control={form.control}
- name={`criteriaDetails.${index}.scoreEquipShip`}
- render={({ field }) => (
- <FormItem>
- <FormLabel>배점/기자재/조선</FormLabel>
- <FormControl>
- <Input
- type="number"
- step="0.1"
- placeholder="배점/기자재/조선"
- {...field}
- value={field.value ?? 0}
- disabled={disabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name={`criteriaDetails.${index}.scoreEquipMarine`}
- render={({ field }) => (
- <FormItem>
- <FormLabel>배점/기자재/해양</FormLabel>
- <FormControl>
- <Input
- type="number"
- step="0.1"
- placeholder="배점/기자재/해양"
- {...field}
- value={field.value ?? 0}
- disabled={disabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name={`criteriaDetails.${index}.scoreBulkShip`}
- render={({ field }) => (
- <FormItem>
- <FormLabel>배점/벌크/조선</FormLabel>
- <FormControl>
- <Input
- type="number"
- step="0.1"
- placeholder="배점/벌크/조선"
- {...field}
- value={field.value ?? 0}
- disabled={disabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name={`criteriaDetails.${index}.scoreBulkMarine`}
- render={({ field }) => (
- <FormItem>
- <FormLabel>배점/벌크/해양</FormLabel>
- <FormControl>
- <Input
- type="number"
- step="0.1"
- placeholder="배점/벌크/해양"
- {...field}
- value={field.value ?? 0}
- disabled={disabled}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
+
+ {/* 고정점수인 경우에만 점수 입력 필드들 표시 - 한 줄에 4개 */}
+ {scoreType === 'fixed' && (
+ <div className="grid grid-cols-4 gap-4">
+ <FormField
+ control={form.control}
+ name={`criteriaDetails.${index}.scoreEquipShip`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>기자재-조선</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ disabled={disabled}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name={`criteriaDetails.${index}.scoreEquipMarine`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>기자재-해양</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ disabled={disabled}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name={`criteriaDetails.${index}.scoreBulkShip`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>벌크-조선</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ disabled={disabled}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name={`criteriaDetails.${index}.scoreBulkMarine`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>벌크-해양</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ disabled={disabled}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ )}
+
+ {/* 변동점수인 경우 안내 메시지 */}
+ {scoreType === 'variable' && (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-sm text-muted-foreground">
+ 변동점수 유형에서는 개별 점수를 입력하지 않습니다.
+ 기본 정보에서 설정한 최소/최대 점수 범위가 적용됩니다.
+ </p>
+ </div>
+ )}
</CardContent>
</Card>
)
@@ -234,9 +260,10 @@ function CriteriaDetailForm({
function RegEvalCriteriaUpdateSheet({
open,
onOpenChange,
- criteriaViewData,
+ criteriaData,
onSuccess,
}: RegEvalCriteriaUpdateSheetProps) {
+
const [isPending, startTransition] = useTransition();
const form = useForm<RegEvalCriteriaFormData>({
resolver: zodResolver(regEvalCriteriaFormSchema),
@@ -247,6 +274,10 @@ function RegEvalCriteriaUpdateSheet({
classification: '',
range: '',
remarks: '',
+ scoreType: 'fixed',
+ variableScoreMin: null,
+ variableScoreMax: null,
+ variableScoreUnit: '',
criteriaDetails: [
{
id: undefined,
@@ -265,11 +296,14 @@ function RegEvalCriteriaUpdateSheet({
name: 'criteriaDetails',
});
+ // 현재 점수 유형 감시
+ const scoreType = form.watch('scoreType');
+
useEffect(() => {
- if (open && criteriaViewData) {
+ if (open && criteriaData?.id) {
startTransition(async () => {
try {
- const targetData = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!);
+ const targetData = await getRegEvalCriteriaWithDetails(criteriaData.id);
if (targetData) {
form.reset({
category: targetData.category,
@@ -278,6 +312,12 @@ function RegEvalCriteriaUpdateSheet({
classification: targetData.classification,
range: targetData.range,
remarks: targetData.remarks,
+ scoreType: targetData.scoreType || 'fixed',
+ variableScoreMin: targetData.variableScoreMin
+ ? Number(targetData.variableScoreMin) : null,
+ variableScoreMax: targetData.variableScoreMax
+ ? Number(targetData.variableScoreMax) : null,
+ variableScoreUnit: targetData.variableScoreUnit,
criteriaDetails: targetData.criteriaDetails?.map((detailItem: RegEvalCriteriaDetails) => ({
id: detailItem.id,
detail: detailItem.detail,
@@ -298,19 +338,27 @@ function RegEvalCriteriaUpdateSheet({
}
});
}
- }, [open, criteriaViewData, form]);
+ }, [open, criteriaData, form]);
const onSubmit = async (data: RegEvalCriteriaFormData) => {
+
startTransition(async () => {
try {
- const criteriaData = {
+ const criteriaDataToUpdate = {
category: data.category,
category2: data.category2,
item: data.item,
classification: data.classification,
range: data.range,
remarks: data.remarks,
+ scoreType: data.scoreType,
+ variableScoreMin: data.variableScoreMin != null
+ ? String(data.variableScoreMin) : null,
+ variableScoreMax: data.variableScoreMax != null
+ ? String(data.variableScoreMax) : null,
+ variableScoreUnit: data.variableScoreUnit,
};
+
const detailList = data.criteriaDetails.map((detailItem) => ({
id: detailItem.id,
detail: detailItem.detail,
@@ -323,14 +371,15 @@ function RegEvalCriteriaUpdateSheet({
scoreBulkMarine: detailItem.scoreBulkMarine != null
? String(detailItem.scoreBulkMarine) : null,
}));
- await modifyRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!, criteriaData, detailList);
- toast.success('평가 기준표가 수정되었습니다.');
+
+ await modifyRegEvalCriteriaWithDetails(criteriaData.id, criteriaDataToUpdate, detailList);
+ toast.success('평가 기준이 수정되었습니다.');
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('Error in Saving Regular Evaluation Criteria:', error);
toast.error(
- error instanceof Error ? error.message : '평가 기준표 저장 중 오류가 발생했습니다.'
+ error instanceof Error ? error.message : '평가 기준 저장 중 오류가 발생했습니다.'
);
}
})
@@ -342,125 +391,225 @@ function RegEvalCriteriaUpdateSheet({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[900px] sm:max-w-[900px] overflow-y-auto">
- <SheetHeader className="mb-4">
+ <SheetContent className="w-[900px] sm:max-w-[900px] flex flex-col" style={{width:900, height: '100vh'}}>
+ {/* 고정 헤더 */}
+ <SheetHeader className="flex-shrink-0 pb-4 border-b">
<SheetTitle className="font-bold">
- 협력업체 평가 기준표 수정
+ 평가 기준 수정
</SheetTitle>
<SheetDescription>
- 협력업체 평가 기준표의 정보를 수정합니다.
+ 평가 기준의 정보를 수정합니다.
</SheetDescription>
</SheetHeader>
+
<Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
- <ScrollArea className="h-[calc(100vh-200px)] pr-4">
- <div className="space-y-6">
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0">
+ {/* 스크롤 가능한 메인 콘텐츠 영역 */}
+ <div className="flex-1 overflow-y-auto py-4 min-h-0">
+ <div className="space-y-6 pr-4">
<Card>
<CardHeader>
- <CardTitle>Criterion Info</CardTitle>
+ <CardTitle>기본 정보</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
- <FormField
- control={form.control}
- name="category"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가부문</FormLabel>
- <FormControl>
- <Select onValueChange={field.onChange} value={field.value || ""}>
- <SelectTrigger>
- <SelectValue placeholder="선택" />
- </SelectTrigger>
- <SelectContent>
- {REG_EVAL_CRITERIA_CATEGORY.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="category2"
- render={({ field }) => (
- <FormItem>
- <FormLabel>점수구분</FormLabel>
- <FormControl>
- <Select onValueChange={field.onChange} value={field.value || ""}>
- <SelectTrigger>
- <SelectValue placeholder="선택" />
- </SelectTrigger>
- <SelectContent>
- {REG_EVAL_CRITERIA_CATEGORY2.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="item"
- render={({ field }) => (
- <FormItem>
- <FormLabel>항목</FormLabel>
- <FormControl>
- <Select onValueChange={field.onChange} value={field.value || ""}>
- <SelectTrigger>
- <SelectValue placeholder="선택" />
- </SelectTrigger>
- <SelectContent>
- {REG_EVAL_CRITERIA_ITEM.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="classification"
- render={({ field }) => (
- <FormItem>
- <FormLabel>구분</FormLabel>
- <FormControl>
- <Input placeholder="구분을 입력하세요." {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="range"
- render={({ field }) => (
- <FormItem>
- <FormLabel>범위</FormLabel>
- <FormControl>
- <Input
- placeholder="범위를 입력하세요." {...field}
- value={field.value ?? ''}
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="category"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가부문</FormLabel>
+ <FormControl>
+ <Select onValueChange={field.onChange} value={field.value || ""}>
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {REG_EVAL_CRITERIA_CATEGORY.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="category2"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>점수구분</FormLabel>
+ <FormControl>
+ <Select onValueChange={field.onChange} value={field.value || ""}>
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {REG_EVAL_CRITERIA_CATEGORY2.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="item"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>항목</FormLabel>
+ <FormControl>
+ <Select onValueChange={field.onChange} value={field.value || ""}>
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {REG_EVAL_CRITERIA_ITEM.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="scoreType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>점수유형</FormLabel>
+ <FormControl>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="fixed">고정점수</SelectItem>
+ <SelectItem value="variable">변동점수</SelectItem>
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="classification"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>구분</FormLabel>
+ <FormControl>
+ <Input placeholder="구분을 입력하세요." {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="range"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가명</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="평가명을 입력하세요." {...field}
+ value={field.value ?? ''}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 변동점수 설정 */}
+ {scoreType === 'variable' && (
+ <>
+ <Separator />
+ <div className="space-y-4">
+ <h4 className="font-medium">변동점수 설정</h4>
+ <div className="grid grid-cols-3 gap-4">
+ <FormField
+ control={form.control}
+ name="variableScoreMin"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>최소점수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
/>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
+ <FormField
+ control={form.control}
+ name="variableScoreMax"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>최대점수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.01"
+ placeholder="0.00"
+ {...field}
+ value={field.value ?? ''}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="variableScoreUnit"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>점수단위</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="예: 점, %"
+ {...field}
+ value={field.value ?? ''}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+ </>
+ )}
+
<FormField
control={form.control}
name="remarks"
@@ -480,13 +629,17 @@ function RegEvalCriteriaUpdateSheet({
/>
</CardContent>
</Card>
+
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
- <CardTitle>Evaluation Criteria Item</CardTitle>
+ <CardTitle>평가 옵션</CardTitle>
<CardDescription>
- Set Evaluation Criteria Item.
+ {scoreType === 'fixed'
+ ? '각 평가 옵션별 점수를 설정하세요.'
+ : '평가 옵션을 설정하세요. (점수는 변동점수 설정을 따릅니다.)'
+ }
</CardDescription>
</div>
<Button
@@ -507,7 +660,7 @@ function RegEvalCriteriaUpdateSheet({
disabled={isPending}
>
<Plus className="w-4 h-4 mr-2" />
- New Item
+ 옵션 추가
</Button>
</div>
</CardHeader>
@@ -521,24 +674,27 @@ function RegEvalCriteriaUpdateSheet({
onRemove={() => remove(index)}
canRemove={fields.length > 1}
disabled={isPending}
+ scoreType={scoreType}
/>
))}
</div>
</CardContent>
</Card>
</div>
- </ScrollArea>
- <div className="flex justify-end gap-2 pt-4 border-t">
+ </div>
+
+ {/* 고정 푸터 */}
+ <div className="flex-shrink-0 flex justify-end gap-2 bg-background">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
- Cancel
+ 취소
</Button>
<Button type="submit" disabled={isPending}>
- {isPending ? 'Saving...' : 'Modify'}
+ {isPending ? '저장 중...' : '수정'}
</Button>
</div>
</form>
diff --git a/lib/evaluation-criteria/validations.ts b/lib/evaluation-criteria/validations.ts
index 39f0580f..1f06c66e 100644
--- a/lib/evaluation-criteria/validations.ts
+++ b/lib/evaluation-criteria/validations.ts
@@ -8,7 +8,7 @@ import {
parseAsStringEnum,
} from 'nuqs/server';
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
-import { type RegEvalCriteriaView } from "@/db/schema";
+import { RegEvalCriteria } from "@/db/schema";
// ----------------------------------------------------------------------------------------------------
@@ -17,14 +17,11 @@ const searchParamsCache = createSearchParamsCache({
flags: parseAsArrayOf(z.enum(['advancedTable', 'floatingBar'])).withDefault([]),
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
- sort: getSortingStateParser<RegEvalCriteriaView>().withDefault([
- { id: 'criteriaId', desc: false },
- { id: 'orderIndex', desc: false },
+ sort: getSortingStateParser<RegEvalCriteria>().withDefault([
+ { id: 'createdAt', desc: false },
+
]),
- tagTypeLabel: parseAsString.withDefault(''),
- classLabel: parseAsString.withDefault(''),
- formCode: parseAsString.withDefault(''),
- formName: parseAsString.withDefault(''),
+
filters: getFiltersStateParser().withDefault([]),
joinOperator: parseAsStringEnum(['and', 'or']).withDefault('and'),
search: parseAsString.withDefault(''),