summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-10 09:55:45 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-10 09:55:45 +0000
commitc657ef972feeafff16ab0e07cb4771f7dd141ba0 (patch)
treebefabd884b00d3cc632c628b3e3810f61cc9f38d /lib/evaluation-target-list
parentb8a03c9d130435a71c5d6217d06ccb0beb9697e5 (diff)
(대표님) 20250710 작업사항 - 평가 첨부, 로그인, SEDP 변경 요구사항 반영
Diffstat (limited to 'lib/evaluation-target-list')
-rw-r--r--lib/evaluation-target-list/service.ts195
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-table.tsx2
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx66
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx181
-rw-r--r--lib/evaluation-target-list/table/update-evaluation-target.tsx10
5 files changed, 380 insertions, 74 deletions
diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts
index 9e21dc51..6de00329 100644
--- a/lib/evaluation-target-list/service.ts
+++ b/lib/evaluation-target-list/service.ts
@@ -1,6 +1,6 @@
'use server'
-import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm";
+import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, gte, lte } from "drizzle-orm";
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers";
import { filterColumns } from "@/lib/filter-columns";
@@ -22,7 +22,9 @@ import {
reviewerEvaluations,
evaluationSubmissions,
generalEvaluations,
- esgEvaluationItems
+ esgEvaluationItems,
+ contracts,
+ projects
} from "@/db/schema";
@@ -33,6 +35,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/route"
import { sendEmail } from "../mail/sendEmail";
import type { SQL } from "drizzle-orm"
import { DEPARTMENT_CODE_LABELS } from "@/types/evaluation";
+import { revalidatePath } from "next/cache";
export async function selectEvaluationTargetsFromView(
tx: PgTransaction<any, any, any>,
@@ -214,8 +217,8 @@ export async function getEvaluationTargetsStats(evaluationYear: number) {
consensusTrue: sql<number>`sum(case when consensus_status = true then 1 else 0 end)`,
consensusFalse: sql<number>`sum(case when consensus_status = false then 1 else 0 end)`,
consensusNull: sql<number>`sum(case when consensus_status is null then 1 else 0 end)`,
- oceanDivision: sql<number>`sum(case when division = 'OCEAN' then 1 else 0 end)`,
- shipyardDivision: sql<number>`sum(case when division = 'SHIPYARD' then 1 else 0 end)`,
+ oceanDivision: sql<number>`sum(case when division = 'PLANT' then 1 else 0 end)`,
+ shipyardDivision: sql<number>`sum(case when division = 'SHIP' then 1 else 0 end)`,
})
.from(evaluationTargetsWithDepartments)
.where(eq(evaluationTargetsWithDepartments.evaluationYear, evaluationYear));
@@ -1165,4 +1168,188 @@ export async function requestEvaluationReview(targetIds: number[], message?: str
error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다."
}
}
+}
+
+
+
+interface AutoGenerateResult {
+ success: boolean
+ message: string
+ error?: string
+ generatedCount?: number
+ skippedCount?: number
+ details?: {
+ shipTargets: number
+ plantTargets: number
+ duplicateSkipped: number
+ }
+}
+
+/**
+ * 자동으로 평가 대상을 생성하는 서버 액션
+ * 전년도 10월부터 현재년도 9월까지의 계약을 기준으로 평가 대상을 생성
+ */
+export async function autoGenerateEvaluationTargets(
+ evaluationYear: number,
+ adminUserId: number
+): Promise<AutoGenerateResult> {
+ try {
+ // 평가 기간 계산 (전년도 10월 ~ 현재년도 9월)
+ const startDate = `${evaluationYear - 1}-10-01`
+ const endDate = `${evaluationYear}-09-30`
+
+ console.log(`Generating evaluation targets for period: ${startDate} to ${endDate}`)
+
+ // 1. 해당 기간의 계약들과 관련 정보를 조회
+ const contractsWithDetails = await db
+ .select({
+ contractId: contracts.id,
+ vendorId: contracts.vendorId,
+ projectId: contracts.projectId,
+ startDate: contracts.startDate,
+ // vendor 정보
+ vendorCode: vendors.vendorCode,
+ vendorName: vendors.vendorName,
+ vendorType: vendors.country ==="KR"? "DOMESTIC":"FOREIGN", // DOMESTIC | FOREIGN
+ // project 정보
+ projectType: projects.type, // ship | plant
+ })
+ .from(contracts)
+ .innerJoin(vendors, eq(contracts.vendorId, vendors.id))
+ .innerJoin(projects, eq(contracts.projectId, projects.id))
+ .where(
+ and(
+ gte(contracts.startDate, startDate),
+ lte(contracts.startDate, endDate)
+ )
+ )
+
+ if (contractsWithDetails.length === 0) {
+ return {
+ success: true,
+ message: "해당 기간에 생성할 평가 대상이 없습니다.",
+ generatedCount: 0,
+ skippedCount: 0
+ }
+ }
+
+ console.log(`Found ${contractsWithDetails.length} contracts in the period`)
+
+ // 2. 벤더별, 구분별로 그룹화하여 중복 제거
+ const targetGroups = new Map<string, {
+ vendorId: number
+ vendorCode: string
+ vendorName: string
+ domesticForeign: "DOMESTIC" | "FOREIGN"
+ division: "SHIP" | "PLANT"
+ materialType: "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK"
+ }>()
+
+ contractsWithDetails.forEach(contract => {
+ const division = contract.projectType === "ship" ? "SHIP" : "PLANT"
+ const key = `${contract.vendorId}-${division}`
+
+ if (!targetGroups.has(key)) {
+ targetGroups.set(key, {
+ vendorId: contract.vendorId,
+ vendorCode: contract.vendorCode,
+ vendorName: contract.vendorName,
+ domesticForeign: contract.vendorType === "DOMESTIC" ? "DOMESTIC" : "FOREIGN",
+ division: division as "SHIP" | "PLANT",
+ // 기본값으로 EQUIPMENT 설정 (추후 더 정교한 로직 필요시 수정)
+ materialType: "EQUIPMENT" as const
+ })
+ }
+ })
+
+ console.log(`Created ${targetGroups.size} unique vendor-division combinations`)
+
+ // 3. 이미 존재하는 평가 대상 확인
+ const existingTargetsKeys = new Set<string>()
+ if (targetGroups.size > 0) {
+ const vendorIds = Array.from(targetGroups.values()).map(t => t.vendorId)
+ const existingTargets = await db
+ .select({
+ vendorId: evaluationTargets.vendorId,
+ division: evaluationTargets.division
+ })
+ .from(evaluationTargets)
+ .where(
+ and(
+ eq(evaluationTargets.evaluationYear, evaluationYear),
+ inArray(evaluationTargets.vendorId, vendorIds)
+ )
+ )
+
+ existingTargets.forEach(target => {
+ existingTargetsKeys.add(`${target.vendorId}-${target.division}`)
+ })
+ }
+
+ console.log(`Found ${existingTargetsKeys.size} existing targets`)
+
+ // 4. 새로운 평가 대상만 필터링
+ const newTargets = Array.from(targetGroups.entries())
+ .filter(([key]) => !existingTargetsKeys.has(key))
+ .map(([_, target]) => target)
+
+ if (newTargets.length === 0) {
+ return {
+ success: true,
+ message: "이미 모든 평가 대상이 생성되어 있습니다.",
+ generatedCount: 0,
+ skippedCount: targetGroups.size
+ }
+ }
+
+ // 5. 평가 대상 생성
+ const evaluationTargetsToInsert = newTargets.map(target => ({
+ evaluationYear,
+ division: target.division,
+ vendorId: target.vendorId,
+ vendorCode: target.vendorCode,
+ vendorName: target.vendorName,
+ domesticForeign: target.domesticForeign,
+ materialType: target.materialType,
+ status: "PENDING" as const,
+ adminUserId,
+ ldClaimCount: 0,
+ ldClaimAmount: "0",
+ ldClaimCurrency: "KRW" as const
+ }))
+
+ // 배치로 삽입
+ await db.insert(evaluationTargets).values(evaluationTargetsToInsert)
+
+ // 통계 계산
+ const shipTargets = newTargets.filter(t => t.division === "SHIP").length
+ const plantTargets = newTargets.filter(t => t.division === "PLANT").length
+ const duplicateSkipped = existingTargetsKeys.size
+
+ console.log(`Successfully created ${newTargets.length} evaluation targets`)
+
+ // 캐시 무효화
+ revalidatePath("/evcp/evaluation-target-list")
+ revalidatePath("/procurement/evaluation-target-list")
+
+ return {
+ success: true,
+ message: `${newTargets.length}개의 평가 대상이 성공적으로 생성되었습니다.`,
+ generatedCount: newTargets.length,
+ skippedCount: duplicateSkipped,
+ details: {
+ shipTargets,
+ plantTargets,
+ duplicateSkipped
+ }
+ }
+
+ } catch (error) {
+ console.error("Error auto generating evaluation targets:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
+ message: "평가 대상 자동 생성에 실패했습니다."
+ }
+ }
} \ No newline at end of file
diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx
index b140df0e..5560d3ff 100644
--- a/lib/evaluation-target-list/table/evaluation-target-table.tsx
+++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx
@@ -271,6 +271,8 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
const [promiseData] = React.use(promises);
const tableData = promiseData;
+ console.log(tableData)
+
/* ---------------------- 검색 파라미터 안전 처리 ---------------------- */
const searchString = React.useMemo(
() => searchParams.toString(), // query가 바뀔 때만 새로 계산
diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
index 60f1af39..c3aa9d71 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
@@ -16,7 +16,7 @@ interface GetColumnsProps {
}
// ✅ 모든 헬퍼 함수들을 컴포넌트 외부로 이동
-const getStatusBadgeVariant = (status: string) => {
+export const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "PENDING": return "secondary";
case "CONFIRMED": return "default";
@@ -43,14 +43,14 @@ const getDivisionBadge = (division: string) => {
);
};
-const getMaterialTypeBadge = (materialType: string) => {
+export const getMaterialTypeBadge = (materialType: string) => {
return <Badge variant="outline">{vendortypeMap[materialType] || materialType}</Badge>;
};
const getDomesticForeignBadge = (domesticForeign: string) => {
return (
<Badge variant={domesticForeign === "DOMESTIC" ? "default" : "secondary"}>
- {domesticForeign === "DOMESTIC" ? "내자" : "외자"}
+ {domesticForeign === "DOMESTIC" ? "D" : "F"}
</Badge>
);
};
@@ -72,6 +72,16 @@ const getEvaluationTargetBadge = (isTarget: boolean | null) => {
);
};
+const getStatusLabel = (status: string) => {
+ const statusMap = {
+ PENDING: "미확정",
+ EXCLUDED: "제외",
+ CONFIRMED: "확정"
+ };
+ return statusMap[status] || status;
+};
+
+
// ✅ 모든 cell 렌더러 함수들을 미리 정의 (매번 새로 생성 방지)
const renderEvaluationYear = ({ row }: any) => (
<span className="font-medium">{row.getValue("evaluationYear")}</span>
@@ -83,7 +93,7 @@ const renderStatus = ({ row }: any) => {
const status = row.getValue<string>("status");
return (
<Badge variant={getStatusBadgeVariant(status)}>
- {status}
+ {getStatusLabel(status)}
</Badge>
);
};
@@ -213,12 +223,7 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
cell: renderStatus,
size: 100,
},
- {
- accessorKey: "consensusStatus",
- header: createHeaderRenderer("의견 일치"),
- cell: renderConsensusStatus,
- size: 100,
- },
+
// 벤더 정보
{
@@ -252,6 +257,47 @@ function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): Col
]
},
+ {
+ accessorKey: "consensusStatus",
+ header: createHeaderRenderer("의견 일치"),
+ cell: renderConsensusStatus,
+ size: 100,
+ },
+
+ {
+ id: "claim",
+ header: "L/D, Claim",
+ columns:[
+ {
+ accessorKey: "ldClaimCount",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="건수" />,
+ cell: ({ row }) => (
+ <span className="text-sm">{row.original.ldClaimCount}</span>
+ ),
+ size: 80,
+ },
+
+ {
+ accessorKey: "ldClaimAmount",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="금액" />,
+ cell: ({ row }) => (
+ <span className="font-mono text-sm">{(Number(row.original.ldClaimAmount).toLocaleString())}</span>
+ ),
+ size: 80,
+ },
+ {
+ accessorKey: "ldClaimCurrency",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="단위" />,
+
+ cell: ({ row }) => (
+ <span className="text-sm">{row.original.ldClaimCurrency}</span>
+ ),
+ size: 80,
+ },
+ ]
+
+ },
+
// 발주 담당자
{
id: "orderReviewer",
diff --git a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
index 8bc5254c..d1c7e500 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
@@ -14,6 +14,7 @@ import {
} from "lucide-react"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
+import { useSession } from "next-auth/react"
import { Button } from "@/components/ui/button"
import {
@@ -31,6 +32,8 @@ import {
} from "./evaluation-target-action-dialogs"
import { EvaluationTargetWithDepartments } from "@/db/schema"
import { exportTableToExcel } from "@/lib/export"
+import { autoGenerateEvaluationTargets } from "../service" // 서버 액션 import
+import { useAuthRole } from "@/hooks/use-auth-role"
interface EvaluationTargetsTableToolbarActionsProps {
table: Table<EvaluationTargetWithDepartments>
@@ -47,6 +50,16 @@ export function EvaluationTargetsTableToolbarActions({
const [excludeDialogOpen, setExcludeDialogOpen] = React.useState(false)
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false)
const router = useRouter()
+ const { data: session } = useSession()
+
+ // 권한 체크
+ const { hasRole, isLoading: roleLoading } = useAuthRole()
+ const canManageEvaluations = hasRole('정기평가') || hasRole('admin')
+
+ // 사용자 ID 가져오기
+ const userId = React.useMemo(() => {
+ return session?.user?.id ? Number(session.user.id) : 1;
+ }, [session]);
// 선택된 행들
const selectedRows = table.getFilteredSelectedRowModel().rows
@@ -141,16 +154,36 @@ export function EvaluationTargetsTableToolbarActions({
const handleAutoGenerate = React.useCallback(async () => {
setIsLoading(true)
try {
- // TODO: 발주실적에서 자동 추출 API 호출
- toast.success("평가 대상이 자동으로 생성되었습니다.")
- router.refresh()
+ // 현재 년도를 기준으로 평가 대상 자동 생성
+ const currentYear = new Date().getFullYear()
+ const result = await autoGenerateEvaluationTargets(currentYear, userId)
+
+ if (result.success) {
+ if (result.generatedCount === 0) {
+ toast.info(result.message, {
+ description: result.skippedCount
+ ? `이미 존재하는 평가 대상: ${result.skippedCount}개`
+ : undefined
+ })
+ } else {
+ toast.success(result.message, {
+ description: result.details
+ ? `해양: ${result.details.shipTargets}개, 조선: ${result.details.plantTargets}개 생성${result.details.duplicateSkipped > 0 ? `, 중복 건너뜀: ${result.details.duplicateSkipped}개` : ''}`
+ : undefined
+ })
+ }
+ onRefresh?.()
+ router.refresh()
+ } else {
+ toast.error(result.error || "자동 생성 중 오류가 발생했습니다.")
+ }
} catch (error) {
console.error('Error auto generating targets:', error)
toast.error("자동 생성 중 오류가 발생했습니다.")
} finally {
setIsLoading(false)
}
- }, [router])
+ }, [router, onRefresh, userId])
// ----------------------------------------------------------------
// 신규 평가 대상 생성 (수동)
@@ -178,33 +211,54 @@ export function EvaluationTargetsTableToolbarActions({
})
}, [table])
+ // 권한이 없거나 로딩 중인 경우 내보내기 버튼만 표시
+ if (roleLoading) {
+ return (
+ <div className="flex items-center gap-2">
+ <div className="flex items-center gap-1 border-l pl-2 ml-2">
+ <Button
+ variant="outline"
+ size="sm"
+ disabled
+ className="gap-2"
+ >
+ <Download className="size-4 animate-spin" aria-hidden="true" />
+ <span className="hidden sm:inline">로딩중...</span>
+ </Button>
+ </div>
+ </div>
+ )
+ }
+
return (
<>
<div className="flex items-center gap-2">
- {/* 신규 생성 드롭다운 */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="default"
- size="sm"
- className="gap-2"
- disabled={isLoading}
- >
- <Plus className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">신규 생성</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="start">
- <DropdownMenuItem onClick={handleAutoGenerate} disabled={isLoading}>
- <RefreshCw className="size-4 mr-2" />
- 자동 생성 (발주실적 기반)
- </DropdownMenuItem>
- <DropdownMenuItem onClick={handleManualCreate}>
- <Plus className="size-4 mr-2" />
- 수동 생성
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
+ {/* 신규 생성 드롭다운 - 정기평가 권한이 있는 경우만 표시 */}
+ {canManageEvaluations && (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="default"
+ size="sm"
+ className="gap-2"
+ disabled={isLoading}
+ >
+ <Plus className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">신규 생성</span>
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="start">
+ <DropdownMenuItem onClick={handleAutoGenerate} disabled={isLoading}>
+ <RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
+ 자동 생성 (발주실적 기반)
+ </DropdownMenuItem>
+ <DropdownMenuItem onClick={handleManualCreate}>
+ <Plus className="size-4 mr-2" />
+ 수동 생성
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )}
{/* 유틸리티 버튼들 */}
<div className="flex items-center gap-1 border-l pl-2 ml-2">
@@ -219,8 +273,8 @@ export function EvaluationTargetsTableToolbarActions({
</Button>
</div>
- {/* 선택된 항목 액션 버튼들 */}
- {hasSelection && (
+ {/* 선택된 항목 액션 버튼들 - 정기평가 권한이 있는 경우만 표시 */}
+ {canManageEvaluations && hasSelection && (
<div className="flex items-center gap-1 border-l pl-2 ml-2">
{/* 확정 버튼 */}
{selectedStats.canConfirm && (
@@ -271,37 +325,52 @@ export function EvaluationTargetsTableToolbarActions({
)}
</div>
)}
+
+ {/* 권한이 없는 경우 안내 메시지 (선택사항) */}
+ {!canManageEvaluations && hasSelection && (
+ <div className="flex items-center gap-1 border-l pl-2 ml-2">
+ <div className="text-xs text-muted-foreground px-2 py-1">
+ 평가 관리 권한이 필요합니다
+ </div>
+ </div>
+ )}
</div>
- {/* 수동 생성 다이얼로그 */}
- <ManualCreateEvaluationTargetDialog
- open={manualCreateDialogOpen}
- onOpenChange={setManualCreateDialogOpen}
- />
+ {/* 다이얼로그들 - 권한이 있는 경우만 렌더링 */}
+ {canManageEvaluations && (
+ <>
+ {/* 수동 생성 다이얼로그 */}
+ <ManualCreateEvaluationTargetDialog
+ open={manualCreateDialogOpen}
+ onOpenChange={setManualCreateDialogOpen}
+ onSuccess={handleActionSuccess}
+ />
- {/* 확정 컨펌 다이얼로그 */}
- <ConfirmTargetsDialog
- open={confirmDialogOpen}
- onOpenChange={setConfirmDialogOpen}
- targets={selectedTargets}
- onSuccess={handleActionSuccess}
- />
+ {/* 확정 컨펌 다이얼로그 */}
+ <ConfirmTargetsDialog
+ open={confirmDialogOpen}
+ onOpenChange={setConfirmDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
- {/* 제외 컨펌 다이얼로그 */}
- <ExcludeTargetsDialog
- open={excludeDialogOpen}
- onOpenChange={setExcludeDialogOpen}
- targets={selectedTargets}
- onSuccess={handleActionSuccess}
- />
+ {/* 제외 컨펌 다이얼로그 */}
+ <ExcludeTargetsDialog
+ open={excludeDialogOpen}
+ onOpenChange={setExcludeDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
- {/* 의견 요청 다이얼로그 */}
- <RequestReviewDialog
- open={reviewDialogOpen}
- onOpenChange={setReviewDialogOpen}
- targets={selectedTargets}
- onSuccess={handleActionSuccess}
- />
+ {/* 의견 요청 다이얼로그 */}
+ <RequestReviewDialog
+ open={reviewDialogOpen}
+ onOpenChange={setReviewDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
+ </>
+ )}
</>
)
} \ No newline at end of file
diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx
index 9f9b7af4..8ea63a1a 100644
--- a/lib/evaluation-target-list/table/update-evaluation-target.tsx
+++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx
@@ -58,6 +58,8 @@ import {
type UpdateEvaluationTargetInput,
} from "../service"
import { EvaluationTargetWithDepartments } from "@/db/schema"
+import { getMaterialTypeBadge } from "./evaluation-targets-columns"
+import { getStatusLabel } from "../validation"
// 편집 가능한 필드들에 대한 스키마
const editEvaluationTargetSchema = z.object({
@@ -123,10 +125,10 @@ export function EditEvaluationTargetSheet({
}
const userEmail = session.user.email
- const userRole = session.user.role
+ const userRole = session.user?.roles
// 평가관리자는 모든 권한
- if (userRole === "평가관리자") {
+ if (userRole?.some(role => role.includes('정기평가'))|| userRole?.some(role => role.toLocaleLowerCase().includes('admin'))) {
return {
level: "admin",
editableApprovals: [
@@ -372,10 +374,10 @@ export function EditEvaluationTargetSheet({
<span className="font-medium">벤더명:</span> {evaluationTarget.vendorName}
</div>
<div>
- <span className="font-medium">자재구분:</span> {evaluationTarget.materialType}
+ <span className="font-medium">자재구분:</span> {getMaterialTypeBadge(evaluationTarget.materialType)}
</div>
<div>
- <span className="font-medium">상태:</span> {evaluationTarget.status}
+ <span className="font-medium">상태:</span> {getStatusLabel(evaluationTarget.status)}
</div>
</div>
</CardContent>