diff options
Diffstat (limited to 'lib')
8 files changed, 817 insertions, 386 deletions
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(''),
|
