summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation/page.tsx194
-rw-r--r--app/[lng]/evcp/(evcp)/project-gtc/page.tsx63
-rw-r--r--app/api/project-gtc/route.ts154
-rw-r--r--components/data-table/use-table-presets.tsx22
-rw-r--r--components/form-data/form-data-table.tsx57
-rw-r--r--components/form-data/spreadJS-dialog.tsx375
-rw-r--r--db/migrations/0157_oval_sunfire.sql14
-rw-r--r--db/migrations/meta/0157_snapshot.json17486
-rw-r--r--db/migrations/meta/_journal.json7
-rw-r--r--db/schema/evaluationCriteria.ts137
-rw-r--r--db/schema/evaluationTarget.ts341
-rw-r--r--db/schema/index.ts1
-rw-r--r--db/schema/projectGtc.ts45
-rw-r--r--db/schema/techSales.ts85
-rw-r--r--lib/evaluation-target-list/service.ts616
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx384
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-table.tsx154
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx459
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx255
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx138
-rw-r--r--lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx151
-rw-r--r--lib/evaluation-target-list/table/update-evaluation-target.tsx760
-rw-r--r--lib/evaluation-target-list/validation.ts12
-rw-r--r--lib/evaluation/service.ts0
-rw-r--r--lib/evaluation/table/evaluation-columns.tsx441
-rw-r--r--lib/evaluation/table/evaluation-filter-sheet.tsx1031
-rw-r--r--lib/evaluation/table/evaluation-table.tsx462
-rw-r--r--lib/evaluation/validation.ts46
-rw-r--r--lib/forms/services.ts38
-rw-r--r--lib/incoterms/table/delete-incoterms-dialog.tsx154
-rw-r--r--lib/incoterms/table/incoterms-add-dialog.tsx12
-rw-r--r--lib/incoterms/table/incoterms-edit-sheet.tsx60
-rw-r--r--lib/incoterms/table/incoterms-table-columns.tsx206
-rw-r--r--lib/incoterms/table/incoterms-table-toolbar.tsx41
-rw-r--r--lib/incoterms/table/incoterms-table.tsx102
-rw-r--r--lib/mail/templates/evaluation-review-request.hbs162
-rw-r--r--lib/payment-terms/table/delete-payment-terms-dialog.tsx154
-rw-r--r--lib/payment-terms/table/payment-terms-add-dialog.tsx10
-rw-r--r--lib/payment-terms/table/payment-terms-edit-sheet.tsx59
-rw-r--r--lib/payment-terms/table/payment-terms-table-columns.tsx206
-rw-r--r--lib/payment-terms/table/payment-terms-table-toolbar.tsx41
-rw-r--r--lib/payment-terms/table/payment-terms-table.tsx102
-rw-r--r--lib/project-gtc/service.ts389
-rw-r--r--lib/project-gtc/table/add-project-dialog.tsx296
-rw-r--r--lib/project-gtc/table/delete-gtc-file-dialog.tsx160
-rw-r--r--lib/project-gtc/table/project-gtc-table-columns.tsx364
-rw-r--r--lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx74
-rw-r--r--lib/project-gtc/table/project-gtc-table.tsx100
-rw-r--r--lib/project-gtc/table/update-gtc-file-sheet.tsx222
-rw-r--r--lib/project-gtc/table/view-gtc-file-dialog.tsx230
-rw-r--r--lib/project-gtc/validations.ts32
-rw-r--r--lib/techsales-rfq/service.ts245
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx88
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx115
54 files changed, 26734 insertions, 818 deletions
diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
new file mode 100644
index 00000000..3ae3272a
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
@@ -0,0 +1,194 @@
+// ================================================================
+// 4. PERIODIC EVALUATIONS PAGE
+// ================================================================
+
+import * as React from "react"
+import { Metadata } from "next"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { Shell } from "@/components/shell"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { HelpCircle } from "lucide-react"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table"
+
+export const metadata: Metadata = {
+ title: "협력업체 정기평가",
+ description: "협력업체 정기평가 진행 현황을 관리합니다.",
+}
+
+interface PeriodicEvaluationsPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+// 프로세스 안내 팝오버 컴포넌트
+function ProcessGuidePopover() {
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button variant="ghost" size="icon" className="h-6 w-6">
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-96" align="start">
+ <div className="space-y-3">
+ <div className="space-y-1">
+ <h4 className="font-medium">정기평가 프로세스</h4>
+ <p className="text-sm text-muted-foreground">
+ 확정된 평가 대상 업체들에 대한 정기평가 절차입니다.
+ </p>
+ </div>
+ <div className="space-y-3 text-sm">
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 1
+ </div>
+ <div>
+ <p className="font-medium">평가 대상 확정</p>
+ <p className="text-muted-foreground">평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 2
+ </div>
+ <div>
+ <p className="font-medium">업체 자료 제출</p>
+ <p className="text-muted-foreground">각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 3
+ </div>
+ <div>
+ <p className="font-medium">평가자 검토</p>
+ <p className="text-muted-foreground">지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.</p>
+ </div>
+ </div>
+ <div className="flex gap-3">
+ <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
+ 4
+ </div>
+ <div>
+ <p className="font-medium">최종 확정</p>
+ <p className="text-muted-foreground">모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
+ )
+}
+
+// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함
+function getDefaultEvaluationYear() {
+ return new Date().getFullYear()
+}
+
+function searchParamsPeriodicEvaluationsCache() {
+ // TODO: 실제 파서 구현
+ return {
+ parse: (params: any) => params
+ }
+}
+
+async function getPeriodicEvaluations(params: any) {
+ // TODO: 실제 API 호출 구현
+ return {
+ data: [],
+ total: 0,
+ pageCount: 0
+ }
+}
+
+export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsPeriodicEvaluationsCache().parse(searchParams)
+ const validFilters = getValidFilters(search.filters || [])
+
+ // 기본 필터 처리
+ let basicFilters = []
+ if (search.basicFilters && search.basicFilters.length > 0) {
+ basicFilters = search.basicFilters
+ }
+
+ // 모든 필터를 합쳐서 처리
+ const allFilters = [...validFilters, ...basicFilters]
+
+ // 조인 연산자
+ const joinOperator = search.basicJoinOperator || search.joinOperator || 'and';
+
+ // 현재 평가년도
+ const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear()
+
+ // Promise.all로 감싸서 전달
+ const promises = Promise.all([
+ getPeriodicEvaluations({
+ ...search,
+ filters: allFilters,
+ joinOperator,
+ })
+ ])
+
+ return (
+ <Shell className="gap-4">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ 협력업체 정기평가
+ </h2>
+ <Badge variant="outline" className="text-sm">
+ {currentEvaluationYear}년도
+ </Badge>
+ <ProcessGuidePopover />
+ </div>
+ </div>
+ </div>
+
+ {/* 메인 테이블 */}
+ <React.Suspense
+ key={JSON.stringify(searchParams)}
+ fallback={
+ <DataTableSkeleton
+ columnCount={15}
+ searchableColumnCount={2}
+ filterableColumnCount={8}
+ cellWidths={[
+ "3rem", // checkbox
+ "5rem", // 평가년도
+ "5rem", // 평가기간
+ "4rem", // 구분
+ "8rem", // 벤더코드
+ "12rem", // 벤더명
+ "4rem", // 내외자
+ "6rem", // 자재구분
+ "5rem", // 문서제출
+ "4rem", // 제출일
+ "4rem", // 마감일
+ "4rem", // 총점
+ "4rem", // 등급
+ "5rem", // 진행상태
+ "8rem" // actions
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <PeriodicEvaluationsTable
+ promises={promises}
+ evaluationYear={currentEvaluationYear}
+ />
+ </React.Suspense>
+ </Shell>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/project-gtc/page.tsx b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx
new file mode 100644
index 00000000..8e12a489
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx
@@ -0,0 +1,63 @@
+import * as React from "react"
+import { type SearchParams } from "@/types/table"
+
+import { Skeleton } from "@/components/ui/skeleton"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { getProjectGtcList } from "@/lib/project-gtc/service"
+import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations"
+import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table"
+
+interface IndexPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function IndexPage(props: IndexPageProps) {
+ const searchParams = await props.searchParams
+ const search = projectGtcSearchParamsSchema.parse(searchParams)
+
+ const promises = Promise.all([
+ getProjectGtcList({
+ page: search.page,
+ perPage: search.perPage,
+ search: search.search,
+ sort: search.sort,
+ }),
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ Project GTC
+ </h2>
+ <p className="text-muted-foreground">
+ 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다.
+ 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
+ {/* 추가 기능이 필요하면 여기에 추가 */}
+ </React.Suspense>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={8}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["3rem", "3rem", "12rem", "20rem", "10rem", "20rem", "15rem", "12rem", "3rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <ProjectGtcTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
+} \ No newline at end of file
diff --git a/app/api/project-gtc/route.ts b/app/api/project-gtc/route.ts
new file mode 100644
index 00000000..8fe4ad2e
--- /dev/null
+++ b/app/api/project-gtc/route.ts
@@ -0,0 +1,154 @@
+import { NextRequest, NextResponse } from "next/server"
+import { uploadProjectGtcFile, deleteProjectGtcFile, getProjectGtcList, getProjectGtcFile } from "@/lib/project-gtc/service"
+import { promises as fs } from "fs"
+import path from "path"
+
+// 파일 다운로드
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const projectId = searchParams.get("projectId")
+ const action = searchParams.get("action")
+
+ // 목록 조회
+ if (action !== "download") {
+ const page = parseInt(searchParams.get("page") || "1")
+ const perPage = parseInt(searchParams.get("perPage") || "10")
+ const search = searchParams.get("search") || ""
+ const sortParam = searchParams.get("sort")
+
+ let sort: Array<{ id: string; desc: boolean }> = []
+ if (sortParam) {
+ try {
+ sort = JSON.parse(sortParam)
+ } catch {
+ sort = [{ id: "projectCreatedAt", desc: true }]
+ }
+ } else {
+ sort = [{ id: "projectCreatedAt", desc: true }]
+ }
+
+ const result = await getProjectGtcList({
+ page,
+ perPage,
+ search,
+ sort,
+ })
+
+ return NextResponse.json(result)
+ }
+
+ // 파일 다운로드
+ if (!projectId) {
+ return NextResponse.json(
+ { error: "프로젝트 ID가 필요합니다." },
+ { status: 400 }
+ )
+ }
+
+ const fileInfo = await getProjectGtcFile(parseInt(projectId))
+
+ if (!fileInfo) {
+ return NextResponse.json(
+ { error: "파일을 찾을 수 없습니다." },
+ { status: 404 }
+ )
+ }
+
+ // 파일 경로 구성
+ const filePath = path.join(process.cwd(), "public", fileInfo.filePath)
+
+ try {
+ // 파일 읽기
+ const fileBuffer = await fs.readFile(filePath)
+
+ // 응답 헤더 설정
+ const headers = new Headers()
+ headers.set('Content-Type', fileInfo.mimeType || 'application/octet-stream')
+ headers.set('Content-Disposition', `attachment; filename="${encodeURIComponent(fileInfo.originalFileName)}"`)
+ headers.set('Content-Length', fileInfo.fileSize?.toString() || '0')
+
+ return new NextResponse(new Uint8Array(fileBuffer), {
+ status: 200,
+ headers,
+ })
+ } catch (fileError) {
+ console.error("파일 읽기 오류:", fileError)
+ return NextResponse.json(
+ { error: "파일을 읽을 수 없습니다." },
+ { status: 500 }
+ )
+ }
+ } catch (error) {
+ console.error("Project GTC API 에러:", error)
+ return NextResponse.json(
+ { error: "요청 처리 중 오류가 발생했습니다." },
+ { status: 500 }
+ )
+ }
+}
+
+// 파일 업로드
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData()
+ const projectId = formData.get("projectId") as string
+ const file = formData.get("file") as File
+
+ if (!projectId || !file) {
+ return NextResponse.json(
+ { error: "프로젝트 ID와 파일이 필요합니다." },
+ { status: 400 }
+ )
+ }
+
+ const result = await uploadProjectGtcFile(parseInt(projectId), file)
+
+ if (result.success) {
+ return NextResponse.json({ success: true, data: result.data })
+ } else {
+ return NextResponse.json(
+ { error: result.error },
+ { status: 400 }
+ )
+ }
+ } catch (error) {
+ console.error("Project GTC 파일 업로드 API 에러:", error)
+ return NextResponse.json(
+ { error: "파일 업로드 중 오류가 발생했습니다." },
+ { status: 500 }
+ )
+ }
+}
+
+// 파일 삭제
+export async function DELETE(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const projectId = searchParams.get("projectId")
+
+ if (!projectId) {
+ return NextResponse.json(
+ { error: "프로젝트 ID가 필요합니다." },
+ { status: 400 }
+ )
+ }
+
+ const result = await deleteProjectGtcFile(parseInt(projectId))
+
+ if (result.success) {
+ return NextResponse.json({ success: true })
+ } else {
+ return NextResponse.json(
+ { error: result.error },
+ { status: 400 }
+ )
+ }
+ } catch (error) {
+ console.error("Project GTC 파일 삭제 API 에러:", error)
+ return NextResponse.json(
+ { error: "파일 삭제 중 오류가 발생했습니다." },
+ { status: 500 }
+ )
+ }
+} \ No newline at end of file
diff --git a/components/data-table/use-table-presets.tsx b/components/data-table/use-table-presets.tsx
index 5e641762..f9ecda2a 100644
--- a/components/data-table/use-table-presets.tsx
+++ b/components/data-table/use-table-presets.tsx
@@ -288,17 +288,19 @@ export function useTablePresets<TData>(
}, [mutate])
// 클라이언트 상태 업데이트 (컬럼 가시성, 핀 등)
- const updateClientState = useCallback(async (newClientState: Partial<TableSettings<TData>>) => {
- if (!activePreset) return
-
- const updatedSettings = {
- ...activePreset.settings,
- ...newClientState
- }
-
- await updatePreset(activePreset.id, updatedSettings)
- }, [activePreset, updatePreset])
+ const updateClientState = useCallback(
+ async (newClientState: Partial<TableSettings<TData>>) => {
+ if (!activePreset) return;
+
+
+ const prev = activePreset.settings;
+ const next = { ...prev, ...newClientState };
+
+ await updatePreset(activePreset.id, next);
+ },
+ [activePreset, updatePreset, tableId] // ← tableId 도 의존성에 넣어 둡니다
+ );
// URL 변경 감지 및 미저장 변경사항 체크
useEffect(() => {
if (!isClient || !presets || !activePreset) return
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 61e9897f..b59684e3 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -57,6 +57,7 @@ import { SEDPConfirmationDialog, SEDPStatusDialog } from "./sedp-components";
import { SEDPCompareDialog } from "./sedp-compare-dialog";
import { getSEDPToken } from "@/lib/sedp/sedp-token";
import { DeleteFormDataDialog } from "./delete-form-data-dialog";
+import { TemplateViewDialog } from "./spreadJS-dialog";
// 기존 fetchTagDataFromSEDP 함수
async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> {
@@ -328,12 +329,12 @@ export default function DynamicTable({
try {
setIsLoadingTemplate(true);
-
+
const templateResult = await fetchTemplateFromSEDP(projectCode, formCode);
-
+
setTemplateData(templateResult);
setTemplateDialogOpen(true);
-
+
toast.success("Template data loaded successfully");
} catch (error) {
console.error("Error fetching template:", error);
@@ -720,21 +721,6 @@ export default function DynamicTable({
</Button>
)}
- {/* 새로 추가된 Template 보기 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleGetTemplate}
- disabled={isAnyOperationPending || selectedRowCount !== 1}
- >
- {isLoadingTemplate ? (
- <Loader className="mr-2 size-4 animate-spin" />
- ) : (
- <Eye className="mr-2 size-4" />
- )}
- View In Spread
- </Button>
-
{/* 버튼 그룹 */}
<div className="flex items-center gap-2">
{/* 태그 관리 드롭다운 */}
@@ -827,7 +813,25 @@ export default function DynamicTable({
Export
</Button>
-
+ {/* 새로 추가된 Template 보기 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleGetTemplate}
+ disabled={isAnyOperationPending || selectedRowCount !== 1}
+ >
+ {isLoadingTemplate ? (
+ <Loader className="mr-2 size-4 animate-spin" />
+ ) : (
+ <Eye className="mr-2 size-4" />
+ )}
+ View Template
+ {selectedRowCount === 1 && (
+ <span className="ml-2 text-xs bg-green-100 text-green-700 px-2 py-1 rounded">
+ 1
+ </span>
+ )}
+ </Button>
{/* COMPARE WITH SEDP 버튼 */}
<Button
@@ -918,6 +922,18 @@ export default function DynamicTable({
templateData={templateData}
selectedRow={selectedRowsData[0]}
formCode={formCode}
+ contractItemId={contractItemId}
+ onUpdateSuccess={(updatedValues) => {
+ // SpreadSheets에서 업데이트된 값을 테이블에 반영
+ const tagNo = updatedValues.TAG_NO;
+ if (tagNo) {
+ setTableData(prev =>
+ prev.map(item =>
+ item.TAG_NO === tagNo ? updatedValues : item
+ )
+ );
+ }
+ }}
/>
{/* SEDP Confirmation Dialog */}
@@ -989,5 +1005,4 @@ export default function DynamicTable({
)}
</>
);
-}
-
+} \ No newline at end of file
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 69232508..4a8550cb 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -1,69 +1,340 @@
+"use client";
+
import * as React from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { GenericData } from "./export-excel-form";
import { SpreadSheets, Worksheet, Column } from "@mescius/spread-sheets-react";
import * as GC from "@mescius/spread-sheets";
+import { toast } from "sonner";
+import { updateFormDataInDB } from "@/lib/forms/services";
+import { Loader, Save } from "lucide-react";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+interface TemplateItem {
+ TMPL_ID: string;
+ NAME: string;
+ TMPL_TYPE: string;
+ SPR_LST_SETUP: {
+ ACT_SHEET: string;
+ HIDN_SHEETS: Array<string>;
+ CONTENT?: string; // SpreadSheets JSON
+ DATA_SHEETS: Array<{
+ SHEET_NAME: string;
+ REG_TYPE_ID: string;
+ MAP_CELL_ATT: Array<{
+ ATT_ID: string;
+ IN: string;
+ }>;
+ }>;
+ };
+ GRD_LST_SETUP: {
+ REG_TYPE_ID: string;
+ SPR_ITM_IDS: Array<string>;
+ ATTS: Array<{}>;
+ };
+ SPR_ITM_LST_SETUP: {
+ ACT_SHEET: string;
+ HIDN_SHEETS: Array<string>;
+ CONTENT?: string; // SpreadSheets JSON
+ DATA_SHEETS: Array<{
+ SHEET_NAME: string;
+ REG_TYPE_ID: string;
+ MAP_CELL_ATT: Array<{
+ ATT_ID: string;
+ IN: string;
+ }>;
+ }>;
+ };
+}
interface TemplateViewDialogProps {
- isOpen: boolean;
- onClose: () => void;
- templateData: any;
- selectedRow: GenericData;
- formCode: string;
- }
+ isOpen: boolean;
+ onClose: () => void;
+ templateData: TemplateItem[] | any; // 배열 또는 기존 형태
+ selectedRow: GenericData;
+ formCode: string;
+ contractItemId: number;
+ /** 업데이트 성공 시 호출될 콜백 */
+ onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
+}
export function TemplateViewDialog({
- isOpen,
- onClose,
- templateData,
- selectedRow,
- formCode
- }: TemplateViewDialogProps) {
- return (
- <Dialog open={isOpen} onOpenChange={onClose}>
- <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{maxWidth:"80vw"}}>
- <DialogHeader className="flex-shrink-0">
- <DialogTitle>SEDP Template - {formCode}</DialogTitle>
- <DialogDescription>
- {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`}
- </DialogDescription>
- </DialogHeader>
-
- {/* 스크롤 가능한 콘텐츠 영역 */}
- <div className="flex-1 overflow-y-auto space-y-4 py-4">
- {/* 여기에 템플릿 데이터나 다른 콘텐츠가 들어갈 예정 */}
- <div className="space-y-4">
- <p className="text-sm text-muted-foreground">
- Template content will be displayed here...
- </p>
-
- {/* 임시로 templateData 표시 */}
- {templateData && (
- <pre className="bg-muted p-4 rounded text-sm overflow-auto">
- {JSON.stringify(templateData, null, 2)}
- </pre>
- )}
+ isOpen,
+ onClose,
+ templateData,
+ selectedRow,
+ formCode,
+ contractItemId,
+ onUpdateSuccess
+}: TemplateViewDialogProps) {
+ const [hostStyle, setHostStyle] = React.useState({
+ width: '100%',
+ height: '100%'
+ });
+
+ const [isPending, setIsPending] = React.useState(false);
+ const [hasChanges, setHasChanges] = React.useState(false);
+ const [currentSpread, setCurrentSpread] = React.useState<any>(null);
+ const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
+
+ // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링
+ const normalizedTemplates = React.useMemo((): TemplateItem[] => {
+ if (!templateData) return [];
+
+ let templates: TemplateItem[];
+ // 이미 배열인 경우
+ if (Array.isArray(templateData)) {
+ templates = templateData as TemplateItem[];
+ } else {
+ // 기존 형태인 경우 (하위 호환성)
+ templates = [templateData as TemplateItem];
+ }
+
+ // CONTENT가 있는 템플릿만 필터링
+ return templates.filter(template => {
+ const sprContent = template.SPR_LST_SETUP?.CONTENT;
+ const sprItmContent = template.SPR_ITM_LST_SETUP?.CONTENT;
+ return sprContent || sprItmContent;
+ });
+ }, [templateData]);
+
+ // 선택된 템플릿 가져오기
+ const selectedTemplate = React.useMemo(() => {
+ if (!selectedTemplateId) return normalizedTemplates[0]; // 기본값: 첫 번째 템플릿
+ return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0];
+ }, [normalizedTemplates, selectedTemplateId]);
+
+ // 템플릿 변경 시 기본 선택
+ React.useEffect(() => {
+ if (normalizedTemplates.length > 0 && !selectedTemplateId) {
+ setSelectedTemplateId(normalizedTemplates[0].TMPL_ID);
+ }
+ }, [normalizedTemplates, selectedTemplateId]);
+
+ const initSpread = React.useCallback((spread: any) => {
+ if (!spread || !selectedTemplate) return;
+
+ try {
+ setCurrentSpread(spread);
+ setHasChanges(false); // 템플릿 로드 시 변경사항 초기화
+
+ // CONTENT 찾기 (SPR_LST_SETUP 또는 SPR_ITM_LST_SETUP 중 하나)
+ let contentJson = null;
+ if (selectedTemplate.SPR_LST_SETUP?.CONTENT) {
+ contentJson = selectedTemplate.SPR_LST_SETUP.CONTENT;
+ console.log('Using SPR_LST_SETUP.CONTENT for template:', selectedTemplate.NAME);
+ } else if (selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
+ contentJson = selectedTemplate.SPR_ITM_LST_SETUP.CONTENT;
+ console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', selectedTemplate.NAME);
+ }
+
+ if (contentJson) {
+ console.log('Loading template content for:', selectedTemplate.NAME);
+
+ const jsonData = typeof contentJson === 'string'
+ ? JSON.parse(contentJson)
+ : contentJson;
+
+ // fromJSON으로 템플릿 구조 로드
+ spread.fromJSON(jsonData);
+ } else {
+ console.warn('No CONTENT found in template:', selectedTemplate.NAME);
+ return;
+ }
+
+ // 값 변경 이벤트 리스너 추가 (간단한 변경사항 감지만)
+ const activeSheet = spread.getActiveSheet();
+
+ activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => {
+ console.log('Cell changed:', info);
+ setHasChanges(true);
+ });
+
+ activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => {
+ console.log('Value changed:', info);
+ setHasChanges(true);
+ });
+
+ } catch (error) {
+ console.error('Error initializing spread:', error);
+ toast.error('Failed to load template');
+ }
+ }, [selectedTemplate]);
+
+ // 템플릿 변경 핸들러
+ const handleTemplateChange = (templateId: string) => {
+ setSelectedTemplateId(templateId);
+ setHasChanges(false); // 템플릿 변경 시 변경사항 초기화
+
+ // SpreadSheets 재초기화는 useCallback 의존성에 의해 자동으로 처리됨
+ if (currentSpread) {
+ // 강제로 재초기화
+ setTimeout(() => {
+ initSpread(currentSpread);
+ }, 100);
+ }
+ };
+
+ // 변경사항 저장 함수
+ const handleSaveChanges = React.useCallback(async () => {
+ if (!currentSpread || !hasChanges) {
+ toast.info("No changes to save");
+ return;
+ }
+
+ try {
+ setIsPending(true);
+
+ // SpreadSheets에서 현재 데이터를 JSON으로 추출
+ const spreadJson = currentSpread.toJSON();
+ console.log('Current spread data:', spreadJson);
+
+ // 실제 데이터 추출 방법은 SpreadSheets 구조에 따라 달라질 수 있음
+ // 여기서는 기본적인 예시만 제공
+ const activeSheet = currentSpread.getActiveSheet();
+
+ // 간단한 예시: 특정 범위의 데이터를 추출하여 selectedRow 형태로 변환
+ // 실제 구현에서는 템플릿의 구조에 맞춰 데이터를 추출해야 함
+ const dataToSave = {
+ ...selectedRow, // 기본값으로 원본 데이터 사용
+ // 여기에 SpreadSheets에서 변경된 값들을 추가
+ // 예: TAG_DESC: activeSheet.getValue(특정행, 특정열)
+ };
+
+ // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정
+ dataToSave.TAG_NO = selectedRow?.TAG_NO;
+
+ console.log('Data to save (TAG_NO preserved):', dataToSave);
+
+ const { success, message } = await updateFormDataInDB(
+ formCode,
+ contractItemId,
+ dataToSave
+ );
+
+ if (!success) {
+ toast.error(message);
+ return;
+ }
+
+ toast.success("Changes saved successfully!");
+
+ const updatedData = {
+ ...selectedRow,
+ ...dataToSave,
+ };
+
+ onUpdateSuccess?.(updatedData);
+ setHasChanges(false);
+
+ } catch (error) {
+ console.error("Error saving changes:", error);
+ toast.error("An unexpected error occurred while saving");
+ } finally {
+ setIsPending(false);
+ }
+ }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess]);
+
+ if (!isOpen) return null;
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent
+ className="w-[80%] max-w-none h-[80vh] flex flex-col"
+ style={{maxWidth:"80vw"}}
+ >
+ <DialogHeader className="flex-shrink-0">
+ <DialogTitle>SEDP Template - {formCode}</DialogTitle>
+ <DialogDescription>
+ {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`}
+ {hasChanges && (
+ <span className="ml-2 text-orange-600 font-medium">
+ • Unsaved changes
+ </span>
+ )}
+ <br />
+ <span className="text-xs text-muted-foreground">
+ Template content will be loaded directly. Manual data entry may be required.
+ </span>
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* 템플릿 선택 UI */}
+ {normalizedTemplates.length > 1 && (
+ <div className="flex-shrink-0 px-4 py-2 border-b">
+ <div className="flex items-center gap-2">
+ <label className="text-sm font-medium">Template:</label>
+ <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
+ <SelectTrigger className="w-64">
+ <SelectValue placeholder="Select a template" />
+ </SelectTrigger>
+ <SelectContent>
+ {normalizedTemplates.map((template) => (
+ <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
+ <div className="flex flex-col">
+ <span>{template.NAME || `Template ${template.TMPL_ID.slice(0, 8)}`}</span>
+ <span className="text-xs text-muted-foreground">{template.TMPL_TYPE}</span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <span className="text-xs text-muted-foreground">
+ ({normalizedTemplates.length} templates available)
+ </span>
</div>
</div>
-
- <DialogFooter className="flex-shrink-0">
- <Button variant="outline" onClick={onClose}>
- Close
- </Button>
+ )}
+
+ {/* SpreadSheets 컴포넌트 영역 */}
+ <div className="flex-1 overflow-hidden">
+ {selectedTemplate ? (
+ <SpreadSheets
+ key={selectedTemplateId} // 템플릿 변경 시 컴포넌트 재생성
+ workbookInitialized={initSpread}
+ hostStyle={hostStyle}
+ />
+ ) : (
+ <div className="flex items-center justify-center h-full text-muted-foreground">
+ No template available
+ </div>
+ )}
+ </div>
+
+ <DialogFooter className="flex-shrink-0">
+ <Button variant="outline" onClick={onClose}>
+ Close
+ </Button>
+
+ {hasChanges && (
<Button
variant="default"
- onClick={() => {
- // 여기에 Template 적용 로직 추가 가능
- console.log('Apply template logic here');
- onClose();
- }}
+ onClick={handleSaveChanges}
+ disabled={isPending}
>
- Apply Template
+ {isPending ? (
+ <>
+ <Loader className="mr-2 h-4 w-4 animate-spin" />
+ Saving...
+ </>
+ ) : (
+ <>
+ <Save className="mr-2 h-4 w-4" />
+ Save Changes
+ </>
+ )}
</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
- } \ No newline at end of file
+ )}
+
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file
diff --git a/db/migrations/0157_oval_sunfire.sql b/db/migrations/0157_oval_sunfire.sql
new file mode 100644
index 00000000..71cb063f
--- /dev/null
+++ b/db/migrations/0157_oval_sunfire.sql
@@ -0,0 +1,14 @@
+CREATE TABLE "project_gtc_files" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "project_id" integer NOT NULL,
+ "file_name" varchar(255) NOT NULL,
+ "file_path" varchar(1024) NOT NULL,
+ "original_file_name" varchar(255) NOT NULL,
+ "file_size" integer,
+ "mime_type" varchar(100),
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "project_gtc_files" ADD CONSTRAINT "project_gtc_files_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+CREATE VIEW "public"."project_gtc_view" AS (select "projects"."id" as "id", "projects"."code" as "code", "projects"."name" as "name", "projects"."type" as "type", "projects"."created_at" as "project_created_at", "projects"."updated_at" as "project_updated_at", "project_gtc_files"."id" as "gtc_file_id", "project_gtc_files"."file_name" as "fileName", "project_gtc_files"."file_path" as "filePath", "project_gtc_files"."original_file_name" as "originalFileName", "project_gtc_files"."file_size" as "fileSize", "project_gtc_files"."mime_type" as "mimeType", "project_gtc_files"."created_at" as "gtcCreatedAt", "project_gtc_files"."updated_at" as "gtcUpdatedAt" from "projects" left join "project_gtc_files" on "projects"."id" = "project_gtc_files"."project_id"); \ No newline at end of file
diff --git a/db/migrations/meta/0157_snapshot.json b/db/migrations/meta/0157_snapshot.json
new file mode 100644
index 00000000..a8f09c62
--- /dev/null
+++ b/db/migrations/meta/0157_snapshot.json
@@ -0,0 +1,17486 @@
+{
+ "id": "daa38495-b1be-415f-a1af-1be1268e9c30",
+ "prevId": "b4eeeb5b-524b-4e3a-bdbc-0ad114343a8f",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.companies": {
+ "name": "companies",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "companies_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "taxID": {
+ "name": "taxID",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contract_envelopes": {
+ "name": "contract_envelopes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "contract_envelopes_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "envelope_id": {
+ "name": "envelope_id",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "envelope_status": {
+ "name": "envelope_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contract_envelopes_contract_id_contracts_id_fk": {
+ "name": "contract_envelopes_contract_id_contracts_id_fk",
+ "tableFrom": "contract_envelopes",
+ "tableTo": "contracts",
+ "columnsFrom": [
+ "contract_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contract_items": {
+ "name": "contract_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "contract_items_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "unit_price": {
+ "name": "unit_price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_rate": {
+ "name": "tax_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_amount": {
+ "name": "tax_amount",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_line_amount": {
+ "name": "total_line_amount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "contract_items_contract_item_idx": {
+ "name": "contract_items_contract_item_idx",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "contract_items_contract_id_contracts_id_fk": {
+ "name": "contract_items_contract_id_contracts_id_fk",
+ "tableFrom": "contract_items",
+ "tableTo": "contracts",
+ "columnsFrom": [
+ "contract_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contract_items_item_id_items_id_fk": {
+ "name": "contract_items_item_id_items_id_fk",
+ "tableFrom": "contract_items",
+ "tableTo": "items",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "contract_items_contract_id_item_id_unique": {
+ "name": "contract_items_contract_id_item_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "contract_id",
+ "item_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contract_signers": {
+ "name": "contract_signers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "contract_signers_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "envelope_id": {
+ "name": "envelope_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_contact_id": {
+ "name": "vendor_contact_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "signer_type": {
+ "name": "signer_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'VENDOR'"
+ },
+ "signer_email": {
+ "name": "signer_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "signer_name": {
+ "name": "signer_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "signer_position": {
+ "name": "signer_position",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "signer_status": {
+ "name": "signer_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'PENDING'"
+ },
+ "signed_at": {
+ "name": "signed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contract_signers_envelope_id_contract_envelopes_id_fk": {
+ "name": "contract_signers_envelope_id_contract_envelopes_id_fk",
+ "tableFrom": "contract_signers",
+ "tableTo": "contract_envelopes",
+ "columnsFrom": [
+ "envelope_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contract_signers_vendor_contact_id_vendor_contacts_id_fk": {
+ "name": "contract_signers_vendor_contact_id_vendor_contacts_id_fk",
+ "tableFrom": "contract_signers",
+ "tableTo": "vendor_contacts",
+ "columnsFrom": [
+ "vendor_contact_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contracts": {
+ "name": "contracts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "contracts_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contract_no": {
+ "name": "contract_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contract_name": {
+ "name": "contract_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_terms": {
+ "name": "payment_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_terms": {
+ "name": "delivery_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_location": {
+ "name": "delivery_location",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'KRW'"
+ },
+ "total_amount": {
+ "name": "total_amount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount": {
+ "name": "discount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax": {
+ "name": "tax",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shipping_fee": {
+ "name": "shipping_fee",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "net_total": {
+ "name": "net_total",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "partial_shipping_allowed": {
+ "name": "partial_shipping_allowed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "partial_payment_allowed": {
+ "name": "partial_payment_allowed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "remarks": {
+ "name": "remarks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contracts_project_id_projects_id_fk": {
+ "name": "contracts_project_id_projects_id_fk",
+ "tableFrom": "contracts",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contracts_vendor_id_vendors_id_fk": {
+ "name": "contracts_vendor_id_vendors_id_fk",
+ "tableFrom": "contracts",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "contracts_contract_no_unique": {
+ "name": "contracts_contract_no_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "contract_no"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.poa": {
+ "name": "poa",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "poa_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_no": {
+ "name": "contract_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_contract_no": {
+ "name": "original_contract_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_contract_name": {
+ "name": "original_contract_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_status": {
+ "name": "original_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivery_terms": {
+ "name": "delivery_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_location": {
+ "name": "delivery_location",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_amount": {
+ "name": "total_amount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount": {
+ "name": "discount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax": {
+ "name": "tax",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shipping_fee": {
+ "name": "shipping_fee",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "net_total": {
+ "name": "net_total",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "change_reason": {
+ "name": "change_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approval_status": {
+ "name": "approval_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'PENDING'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "poa_original_contract_no_contracts_contract_no_fk": {
+ "name": "poa_original_contract_no_contracts_contract_no_fk",
+ "tableFrom": "poa",
+ "tableTo": "contracts",
+ "columnsFrom": [
+ "original_contract_no"
+ ],
+ "columnsTo": [
+ "contract_no"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "poa_project_id_projects_id_fk": {
+ "name": "poa_project_id_projects_id_fk",
+ "tableFrom": "poa",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "poa_vendor_id_vendors_id_fk": {
+ "name": "poa_vendor_id_vendors_id_fk",
+ "tableFrom": "poa",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item_offshore_hull": {
+ "name": "item_offshore_hull",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "work_type": {
+ "name": "work_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_list": {
+ "name": "item_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sub_item_list": {
+ "name": "sub_item_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item_offshore_top": {
+ "name": "item_offshore_top",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "work_type": {
+ "name": "work_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_list": {
+ "name": "item_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sub_item_list": {
+ "name": "sub_item_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item_shipbuilding": {
+ "name": "item_shipbuilding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "work_type": {
+ "name": "work_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_list": {
+ "name": "item_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ship_types": {
+ "name": "ship_types",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'OPTION'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.items": {
+ "name": "items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_no": {
+ "name": "project_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "package_code": {
+ "name": "package_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sm_code": {
+ "name": "sm_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_item_code": {
+ "name": "parent_item_code",
+ "type": "varchar(18)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_level": {
+ "name": "item_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delete_flag": {
+ "name": "delete_flag",
+ "type": "varchar(1)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_of_measure": {
+ "name": "unit_of_measure",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "steel_type": {
+ "name": "steel_type",
+ "type": "varchar(2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grade_material": {
+ "name": "grade_material",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "change_date": {
+ "name": "change_date",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "base_unit_of_measure": {
+ "name": "base_unit_of_measure",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "items_item_code_unique": {
+ "name": "items_item_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "item_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.materials": {
+ "name": "materials",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_item_code": {
+ "name": "parent_item_code",
+ "type": "varchar(18)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_level": {
+ "name": "item_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delete_flag": {
+ "name": "delete_flag",
+ "type": "varchar(1)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_of_measure": {
+ "name": "unit_of_measure",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "steel_type": {
+ "name": "steel_type",
+ "type": "varchar(2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grade_material": {
+ "name": "grade_material",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "change_date": {
+ "name": "change_date",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "base_unit_of_measure": {
+ "name": "base_unit_of_measure",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "materials_item_code_unique": {
+ "name": "materials_item_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "item_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pq_criterias": {
+ "name": "pq_criterias",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "check_point": {
+ "name": "check_point",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remarks": {
+ "name": "remarks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "group_name": {
+ "name": "group_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pq_criterias_extension": {
+ "name": "pq_criterias_extension",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "pq_criteria_id": {
+ "name": "pq_criteria_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contract_info": {
+ "name": "contract_info",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "additional_requirement": {
+ "name": "additional_requirement",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pq_criterias_extension_pq_criteria_id_pq_criterias_id_fk": {
+ "name": "pq_criterias_extension_pq_criteria_id_pq_criterias_id_fk",
+ "tableFrom": "pq_criterias_extension",
+ "tableTo": "pq_criterias",
+ "columnsFrom": [
+ "pq_criteria_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "pq_criterias_extension_project_id_projects_id_fk": {
+ "name": "pq_criterias_extension_project_id_projects_id_fk",
+ "tableFrom": "pq_criterias_extension",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_criteria_attachments": {
+ "name": "vendor_criteria_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_criteria_answer_id": {
+ "name": "vendor_criteria_answer_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_criteria_attachments_vendor_criteria_answer_id_vendor_pq_criteria_answers_id_fk": {
+ "name": "vendor_criteria_attachments_vendor_criteria_answer_id_vendor_pq_criteria_answers_id_fk",
+ "tableFrom": "vendor_criteria_attachments",
+ "tableTo": "vendor_pq_criteria_answers",
+ "columnsFrom": [
+ "vendor_criteria_answer_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_investigation_attachments": {
+ "name": "vendor_investigation_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "investigation_id": {
+ "name": "investigation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'REPORT'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_investigation_attachments_investigation_id_vendor_investigations_id_fk": {
+ "name": "vendor_investigation_attachments_investigation_id_vendor_investigations_id_fk",
+ "tableFrom": "vendor_investigation_attachments",
+ "tableTo": "vendor_investigations",
+ "columnsFrom": [
+ "investigation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_investigations": {
+ "name": "vendor_investigations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pq_submission_id": {
+ "name": "pq_submission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requester_id": {
+ "name": "requester_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "qm_manager_id": {
+ "name": "qm_manager_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_status": {
+ "name": "investigation_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PLANNED'"
+ },
+ "evaluation_type": {
+ "name": "evaluation_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_address": {
+ "name": "investigation_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_method": {
+ "name": "investigation_method",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduled_start_at": {
+ "name": "scheduled_start_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduled_end_at": {
+ "name": "scheduled_end_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forecasted_at": {
+ "name": "forecasted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_at": {
+ "name": "confirmed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "evaluation_score": {
+ "name": "evaluation_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "evaluation_result": {
+ "name": "evaluation_result",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_notes": {
+ "name": "investigation_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_investigations_vendor_id_vendors_id_fk": {
+ "name": "vendor_investigations_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_investigations",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "vendor_investigations_pq_submission_id_vendor_pq_submissions_id_fk": {
+ "name": "vendor_investigations_pq_submission_id_vendor_pq_submissions_id_fk",
+ "tableFrom": "vendor_investigations",
+ "tableTo": "vendor_pq_submissions",
+ "columnsFrom": [
+ "pq_submission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "cascade"
+ },
+ "vendor_investigations_requester_id_users_id_fk": {
+ "name": "vendor_investigations_requester_id_users_id_fk",
+ "tableFrom": "vendor_investigations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "requester_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "vendor_investigations_qm_manager_id_users_id_fk": {
+ "name": "vendor_investigations_qm_manager_id_users_id_fk",
+ "tableFrom": "vendor_investigations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "qm_manager_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_pq_submissions": {
+ "name": "vendor_pq_submissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "pq_number": {
+ "name": "pq_number",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "requester_id": {
+ "name": "requester_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'REQUESTED'"
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejected_at": {
+ "name": "rejected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reject_reason": {
+ "name": "reject_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "unique_pq_submission": {
+ "name": "unique_pq_submission",
+ "columns": [
+ {
+ "expression": "vendor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "vendor_pq_submissions_requester_id_users_id_fk": {
+ "name": "vendor_pq_submissions_requester_id_users_id_fk",
+ "tableFrom": "vendor_pq_submissions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "requester_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "vendor_pq_submissions_vendor_id_vendors_id_fk": {
+ "name": "vendor_pq_submissions_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_pq_submissions",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "vendor_pq_submissions_project_id_projects_id_fk": {
+ "name": "vendor_pq_submissions_project_id_projects_id_fk",
+ "tableFrom": "vendor_pq_submissions",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "vendor_pq_submissions_pq_number_unique": {
+ "name": "vendor_pq_submissions_pq_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "pq_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_pq_criteria_answers": {
+ "name": "vendor_pq_criteria_answers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "criteria_id": {
+ "name": "criteria_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "answer": {
+ "name": "answer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_pq_criteria_answers_vendor_id_vendors_id_fk": {
+ "name": "vendor_pq_criteria_answers_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_pq_criteria_answers",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "vendor_pq_criteria_answers_criteria_id_pq_criterias_id_fk": {
+ "name": "vendor_pq_criteria_answers_criteria_id_pq_criterias_id_fk",
+ "tableFrom": "vendor_pq_criteria_answers",
+ "tableTo": "pq_criterias",
+ "columnsFrom": [
+ "criteria_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "vendor_pq_criteria_answers_project_id_projects_id_fk": {
+ "name": "vendor_pq_criteria_answers_project_id_projects_id_fk",
+ "tableFrom": "vendor_pq_criteria_answers",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_pq_review_logs": {
+ "name": "vendor_pq_review_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_pq_criteria_answer_id": {
+ "name": "vendor_pq_criteria_answer_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reviewer_comment": {
+ "name": "reviewer_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reviewer_name": {
+ "name": "reviewer_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_pq_review_logs_vendor_pq_criteria_answer_id_vendor_pq_criteria_answers_id_fk": {
+ "name": "vendor_pq_review_logs_vendor_pq_criteria_answer_id_vendor_pq_criteria_answers_id_fk",
+ "tableFrom": "vendor_pq_review_logs",
+ "tableTo": "vendor_pq_criteria_answers",
+ "columnsFrom": [
+ "vendor_pq_criteria_answer_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_project_pqs": {
+ "name": "vendor_project_pqs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'REQUESTED'"
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejected_at": {
+ "name": "rejected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reject_reason": {
+ "name": "reject_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_project_pqs_vendor_id_vendors_id_fk": {
+ "name": "vendor_project_pqs_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_project_pqs",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "vendor_project_pqs_project_id_projects_id_fk": {
+ "name": "vendor_project_pqs_project_id_projects_id_fk",
+ "tableFrom": "vendor_project_pqs",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.bidding_projects": {
+ "name": "bidding_projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "pspid": {
+ "name": "pspid",
+ "type": "char(24)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "proj_nm": {
+ "name": "proj_nm",
+ "type": "varchar(90)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sector": {
+ "name": "sector",
+ "type": "char(1)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "proj_msrm": {
+ "name": "proj_msrm",
+ "type": "numeric(3, 0)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kunnr": {
+ "name": "kunnr",
+ "type": "char(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kunnr_nm": {
+ "name": "kunnr_nm",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cls_1": {
+ "name": "cls_1",
+ "type": "char(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cls1_nm": {
+ "name": "cls1_nm",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ptype": {
+ "name": "ptype",
+ "type": "char(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ptype_nm": {
+ "name": "ptype_nm",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pmodel_cd": {
+ "name": "pmodel_cd",
+ "type": "char(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pmodel_nm": {
+ "name": "pmodel_nm",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pmodel_sz": {
+ "name": "pmodel_sz",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pmodel_uom": {
+ "name": "pmodel_uom",
+ "type": "char(5)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "txt04": {
+ "name": "txt04",
+ "type": "char(4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "txt30": {
+ "name": "txt30",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "estm_pm": {
+ "name": "estm_pm",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "bidding_projects_pspid_unique": {
+ "name": "bidding_projects_pspid_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "pspid"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project_series": {
+ "name": "project_series",
+ "schema": "",
+ "columns": {
+ "pspid": {
+ "name": "pspid",
+ "type": "char(24)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sers_no": {
+ "name": "sers_no",
+ "type": "char(3)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sc_dt": {
+ "name": "sc_dt",
+ "type": "char(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kl_dt": {
+ "name": "kl_dt",
+ "type": "char(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lc_dt": {
+ "name": "lc_dt",
+ "type": "char(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dl_dt": {
+ "name": "dl_dt",
+ "type": "char(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dock_no": {
+ "name": "dock_no",
+ "type": "char(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dock_nm": {
+ "name": "dock_nm",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "proj_no": {
+ "name": "proj_no",
+ "type": "char(24)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "post1": {
+ "name": "post1",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "project_sersNo_unique": {
+ "name": "project_sersNo_unique",
+ "columns": [
+ {
+ "expression": "pspid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sers_no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "project_series_pspid_bidding_projects_pspid_fk": {
+ "name": "project_series_pspid_bidding_projects_pspid_fk",
+ "tableFrom": "project_series",
+ "tableTo": "bidding_projects",
+ "columnsFrom": [
+ "pspid"
+ ],
+ "columnsTo": [
+ "pspid"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ship'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cbe_evaluations": {
+ "name": "cbe_evaluations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluated_by": {
+ "name": "evaluated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "evaluated_at": {
+ "name": "evaluated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "result": {
+ "name": "result",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USD'"
+ },
+ "payment_terms": {
+ "name": "payment_terms",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms": {
+ "name": "incoterms",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_schedule": {
+ "name": "delivery_schedule",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "cbe_evaluations_rfq_id_rfqs_id_fk": {
+ "name": "cbe_evaluations_rfq_id_rfqs_id_fk",
+ "tableFrom": "cbe_evaluations",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "cbe_evaluations_vendor_id_vendors_id_fk": {
+ "name": "cbe_evaluations_vendor_id_vendors_id_fk",
+ "tableFrom": "cbe_evaluations",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "cbe_evaluations_evaluated_by_users_id_fk": {
+ "name": "cbe_evaluations_evaluated_by_users_id_fk",
+ "tableFrom": "cbe_evaluations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "evaluated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rfq_attachments": {
+ "name": "rfq_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_id": {
+ "name": "evaluation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cbe_id": {
+ "name": "cbe_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rfq_attachments_rfq_id_rfqs_id_fk": {
+ "name": "rfq_attachments_rfq_id_rfqs_id_fk",
+ "tableFrom": "rfq_attachments",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_attachments_vendor_id_vendors_id_fk": {
+ "name": "rfq_attachments_vendor_id_vendors_id_fk",
+ "tableFrom": "rfq_attachments",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_attachments_evaluation_id_rfq_evaluations_id_fk": {
+ "name": "rfq_attachments_evaluation_id_rfq_evaluations_id_fk",
+ "tableFrom": "rfq_attachments",
+ "tableTo": "rfq_evaluations",
+ "columnsFrom": [
+ "evaluation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_attachments_cbe_id_cbe_evaluations_id_fk": {
+ "name": "rfq_attachments_cbe_id_cbe_evaluations_id_fk",
+ "tableFrom": "rfq_attachments",
+ "tableTo": "cbe_evaluations",
+ "columnsFrom": [
+ "cbe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_attachments_comment_id_rfq_comments_id_fk": {
+ "name": "rfq_attachments_comment_id_rfq_comments_id_fk",
+ "tableFrom": "rfq_attachments",
+ "tableTo": "rfq_comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rfq_comments": {
+ "name": "rfq_comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_text": {
+ "name": "comment_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "commented_by": {
+ "name": "commented_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_id": {
+ "name": "evaluation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cbe_id": {
+ "name": "cbe_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rfq_comments_rfq_id_rfqs_id_fk": {
+ "name": "rfq_comments_rfq_id_rfqs_id_fk",
+ "tableFrom": "rfq_comments",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_comments_vendor_id_vendors_id_fk": {
+ "name": "rfq_comments_vendor_id_vendors_id_fk",
+ "tableFrom": "rfq_comments",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_comments_commented_by_users_id_fk": {
+ "name": "rfq_comments_commented_by_users_id_fk",
+ "tableFrom": "rfq_comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "commented_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_comments_evaluation_id_rfq_evaluations_id_fk": {
+ "name": "rfq_comments_evaluation_id_rfq_evaluations_id_fk",
+ "tableFrom": "rfq_comments",
+ "tableTo": "rfq_evaluations",
+ "columnsFrom": [
+ "evaluation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_comments_cbe_id_vendor_responses_id_fk": {
+ "name": "rfq_comments_cbe_id_vendor_responses_id_fk",
+ "tableFrom": "rfq_comments",
+ "tableTo": "vendor_responses",
+ "columnsFrom": [
+ "cbe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rfq_evaluations": {
+ "name": "rfq_evaluations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "eval_type": {
+ "name": "eval_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result": {
+ "name": "result",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rfq_evaluations_rfq_id_rfqs_id_fk": {
+ "name": "rfq_evaluations_rfq_id_rfqs_id_fk",
+ "tableFrom": "rfq_evaluations",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rfq_evaluations_vendor_id_vendors_id_fk": {
+ "name": "rfq_evaluations_vendor_id_vendors_id_fk",
+ "tableFrom": "rfq_evaluations",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rfq_items": {
+ "name": "rfq_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "uom": {
+ "name": "uom",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rfq_items_rfq_id_rfqs_id_fk": {
+ "name": "rfq_items_rfq_id_rfqs_id_fk",
+ "tableFrom": "rfq_items",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "rfq_items_item_code_items_item_code_fk": {
+ "name": "rfq_items_item_code_items_item_code_fk",
+ "tableFrom": "rfq_items",
+ "tableTo": "items",
+ "columnsFrom": [
+ "item_code"
+ ],
+ "columnsTo": [
+ "item_code"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rfqs": {
+ "name": "rfqs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bid_project_id": {
+ "name": "bid_project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'DRAFT'"
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'PURCHASE'"
+ },
+ "parent_rfq_id": {
+ "name": "parent_rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rfqs_project_id_projects_id_fk": {
+ "name": "rfqs_project_id_projects_id_fk",
+ "tableFrom": "rfqs",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "rfqs_bid_project_id_bidding_projects_id_fk": {
+ "name": "rfqs_bid_project_id_bidding_projects_id_fk",
+ "tableFrom": "rfqs",
+ "tableTo": "bidding_projects",
+ "columnsFrom": [
+ "bid_project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "rfqs_created_by_users_id_fk": {
+ "name": "rfqs_created_by_users_id_fk",
+ "tableFrom": "rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "rfqs_parent_rfq_id_rfqs_id_fk": {
+ "name": "rfqs_parent_rfq_id_rfqs_id_fk",
+ "tableFrom": "rfqs",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "parent_rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "rfqs_rfq_code_unique": {
+ "name": "rfqs_rfq_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "rfq_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_commercial_responses": {
+ "name": "vendor_commercial_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "total_price": {
+ "name": "total_price",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USD'"
+ },
+ "payment_terms": {
+ "name": "payment_terms",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms": {
+ "name": "incoterms",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_period": {
+ "name": "delivery_period",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "warranty_period": {
+ "name": "warranty_period",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "validity_period": {
+ "name": "validity_period",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "price_breakdown": {
+ "name": "price_breakdown",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commercial_notes": {
+ "name": "commercial_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_commercial_responses_response_id_vendor_responses_id_fk": {
+ "name": "vendor_commercial_responses_response_id_vendor_responses_id_fk",
+ "tableFrom": "vendor_commercial_responses",
+ "tableTo": "vendor_responses",
+ "columnsFrom": [
+ "response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_response_attachments": {
+ "name": "vendor_response_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "technical_response_id": {
+ "name": "technical_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commercial_response_id": {
+ "name": "commercial_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_response_attachments_response_id_vendor_responses_id_fk": {
+ "name": "vendor_response_attachments_response_id_vendor_responses_id_fk",
+ "tableFrom": "vendor_response_attachments",
+ "tableTo": "vendor_responses",
+ "columnsFrom": [
+ "response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_response_attachments_technical_response_id_vendor_technical_responses_id_fk": {
+ "name": "vendor_response_attachments_technical_response_id_vendor_technical_responses_id_fk",
+ "tableFrom": "vendor_response_attachments",
+ "tableTo": "vendor_technical_responses",
+ "columnsFrom": [
+ "technical_response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_response_attachments_commercial_response_id_vendor_commercial_responses_id_fk": {
+ "name": "vendor_response_attachments_commercial_response_id_vendor_commercial_responses_id_fk",
+ "tableFrom": "vendor_response_attachments",
+ "tableTo": "vendor_commercial_responses",
+ "columnsFrom": [
+ "commercial_response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_responses": {
+ "name": "vendor_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'REVIEWING'"
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_by": {
+ "name": "responded_by",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_at": {
+ "name": "responded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "vendor_response_unique": {
+ "name": "vendor_response_unique",
+ "columns": [
+ {
+ "expression": "rfq_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "vendor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "vendor_responses_rfq_id_rfqs_id_fk": {
+ "name": "vendor_responses_rfq_id_rfqs_id_fk",
+ "tableFrom": "vendor_responses",
+ "tableTo": "rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_responses_vendor_id_vendors_id_fk": {
+ "name": "vendor_responses_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_responses",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_technical_responses": {
+ "name": "vendor_technical_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_technical_responses_response_id_vendor_responses_id_fk": {
+ "name": "vendor_technical_responses_response_id_vendor_responses_id_fk",
+ "tableFrom": "vendor_technical_responses",
+ "tableTo": "vendor_responses",
+ "columnsFrom": [
+ "response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.departments": {
+ "name": "departments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "department_code": {
+ "name": "department_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department_name": {
+ "name": "department_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "departments_department_code_unique": {
+ "name": "departments_department_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "department_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.otps": {
+ "name": "otps",
+ "schema": "",
+ "columns": {
+ "email": {
+ "name": "email",
+ "type": "varchar(256)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(6)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "otpToken": {
+ "name": "otpToken",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "otp_expires": {
+ "name": "otp_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "permissions_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "permission_key": {
+ "name": "permission_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.role_permissions": {
+ "name": "role_permissions",
+ "schema": "",
+ "columns": {
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_id": {
+ "name": "permission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "role_permissions_role_id_roles_id_fk": {
+ "name": "role_permissions_role_id_roles_id_fk",
+ "tableFrom": "role_permissions",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "role_permissions_permission_id_permissions_id_fk": {
+ "name": "role_permissions_permission_id_permissions_id_fk",
+ "tableFrom": "role_permissions",
+ "tableTo": "permissions",
+ "columnsFrom": [
+ "permission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles": {
+ "name": "roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "roles_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "user_domain",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "roles_company_id_vendors_id_fk": {
+ "name": "roles_company_id_vendors_id_fk",
+ "tableFrom": "roles",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_roles": {
+ "name": "user_roles",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_roles_user_id_users_id_fk": {
+ "name": "user_roles_user_id_users_id_fk",
+ "tableFrom": "user_roles",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_roles_role_id_roles_id_fk": {
+ "name": "user_roles_role_id_roles_id_fk",
+ "tableFrom": "user_roles",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "users_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tech_company_id": {
+ "name": "tech_company_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "user_domain",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'partners'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'en'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_company_id_vendors_id_fk": {
+ "name": "users_company_id_vendors_id_fk",
+ "tableFrom": "users",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "users_tech_company_id_tech_vendors_id_fk": {
+ "name": "users_tech_company_id_tech_vendors_id_fk",
+ "tableFrom": "users",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "tech_company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.form_entries": {
+ "name": "form_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "form_code": {
+ "name": "form_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contract_item_id": {
+ "name": "contract_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "form_entries_contract_item_id_contract_items_id_fk": {
+ "name": "form_entries_contract_item_id_contract_items_id_fk",
+ "tableFrom": "form_entries",
+ "tableTo": "contract_items",
+ "columnsFrom": [
+ "contract_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.form_metas": {
+ "name": "form_metas",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_code": {
+ "name": "form_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_name": {
+ "name": "form_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "columns": {
+ "name": "columns",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "form_metas_project_id_projects_id_fk": {
+ "name": "form_metas_project_id_projects_id_fk",
+ "tableFrom": "form_metas",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "form_code_project_unique": {
+ "name": "form_code_project_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "form_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.forms": {
+ "name": "forms",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "forms_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_item_id": {
+ "name": "contract_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_code": {
+ "name": "form_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_name": {
+ "name": "form_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "eng": {
+ "name": "eng",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "im": {
+ "name": "im",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "contract_item_form_code_unique": {
+ "name": "contract_item_form_code_unique",
+ "columns": [
+ {
+ "expression": "contract_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "form_code",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "forms_contract_item_id_contract_items_id_fk": {
+ "name": "forms_contract_item_id_contract_items_id_fk",
+ "tableFrom": "forms",
+ "tableTo": "contract_items",
+ "columnsFrom": [
+ "contract_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_class_attributes": {
+ "name": "tag_class_attributes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "tag_class_attributes_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "tag_class_id": {
+ "name": "tag_class_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "att_id": {
+ "name": "att_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "def_val": {
+ "name": "def_val",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uom_id": {
+ "name": "uom_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seq": {
+ "name": "seq",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tag_class_attributes_seq_idx": {
+ "name": "tag_class_attributes_seq_idx",
+ "columns": [
+ {
+ "expression": "seq",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_class_attributes_tag_class_id_tag_classes_id_fk": {
+ "name": "tag_class_attributes_tag_class_id_tag_classes_id_fk",
+ "tableFrom": "tag_class_attributes",
+ "tableTo": "tag_classes",
+ "columnsFrom": [
+ "tag_class_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uniq_att_id_in_tag_class": {
+ "name": "uniq_att_id_in_tag_class",
+ "nullsNotDistinct": false,
+ "columns": [
+ "tag_class_id",
+ "att_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_classes": {
+ "name": "tag_classes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "tag_classes_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_type_code": {
+ "name": "tag_type_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tag_classes_project_id_projects_id_fk": {
+ "name": "tag_classes_project_id_projects_id_fk",
+ "tableFrom": "tag_classes",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tag_classes_tag_type_code_project_id_tag_types_code_project_id_fk": {
+ "name": "tag_classes_tag_type_code_project_id_tag_types_code_project_id_fk",
+ "tableFrom": "tag_classes",
+ "tableTo": "tag_types",
+ "columnsFrom": [
+ "tag_type_code",
+ "project_id"
+ ],
+ "columnsTo": [
+ "code",
+ "project_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uniq_code_in_project": {
+ "name": "uniq_code_in_project",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_subfield_options": {
+ "name": "tag_subfield_options",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes_id": {
+ "name": "attributes_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tag_subfield_options_project_id_projects_id_fk": {
+ "name": "tag_subfield_options_project_id_projects_id_fk",
+ "tableFrom": "tag_subfield_options",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uniq_attribute_project_code": {
+ "name": "uniq_attribute_project_code",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "attributes_id",
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_subfields": {
+ "name": "tag_subfields",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_type_code": {
+ "name": "tag_type_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes_id": {
+ "name": "attributes_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes_description": {
+ "name": "attributes_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expression": {
+ "name": "expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delimiter": {
+ "name": "delimiter",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tag_subfields_project_id_projects_id_fk": {
+ "name": "tag_subfields_project_id_projects_id_fk",
+ "tableFrom": "tag_subfields",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uniq_tag_type_attribute": {
+ "name": "uniq_tag_type_attribute",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "tag_type_code",
+ "attributes_id"
+ ]
+ },
+ "uniq_attribute_id_project": {
+ "name": "uniq_attribute_id_project",
+ "nullsNotDistinct": false,
+ "columns": [
+ "attributes_id",
+ "project_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_type_class_form_mappings": {
+ "name": "tag_type_class_form_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_type_label": {
+ "name": "tag_type_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "class_label": {
+ "name": "class_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_code": {
+ "name": "form_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_name": {
+ "name": "form_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ep": {
+ "name": "ep",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uniq_mapping_in_project": {
+ "name": "uniq_mapping_in_project",
+ "nullsNotDistinct": false,
+ "columns": [
+ "project_id",
+ "tag_type_label",
+ "class_label",
+ "form_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_types": {
+ "name": "tag_types",
+ "schema": "",
+ "columns": {
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tag_types_project_id_projects_id_fk": {
+ "name": "tag_types_project_id_projects_id_fk",
+ "tableFrom": "tag_types",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "tag_types_code_project_id_pk": {
+ "name": "tag_types_code_project_id_pk",
+ "columns": [
+ "code",
+ "project_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "tags_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_item_id": {
+ "name": "contract_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag_no": {
+ "name": "tag_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_type": {
+ "name": "tag_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "class": {
+ "name": "class",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tags_contract_item_id_contract_items_id_fk": {
+ "name": "tags_contract_item_id_contract_items_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "contract_items",
+ "columnsFrom": [
+ "contract_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tags_form_id_forms_id_fk": {
+ "name": "tags_form_id_forms_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "forms",
+ "columnsFrom": [
+ "form_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "contract_item_tag_no_unique": {
+ "name": "contract_item_tag_no_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "contract_item_id",
+ "tag_no"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_data_report_temps": {
+ "name": "vendor_data_report_temps",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contract_item_id": {
+ "name": "contract_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_data_report_temps_contract_item_id_contract_items_id_fk": {
+ "name": "vendor_data_report_temps_contract_item_id_contract_items_id_fk",
+ "tableFrom": "vendor_data_report_temps",
+ "tableTo": "contract_items",
+ "columnsFrom": [
+ "contract_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_data_report_temps_form_id_forms_id_fk": {
+ "name": "vendor_data_report_temps_form_id_forms_id_fk",
+ "tableFrom": "vendor_data_report_temps",
+ "tableTo": "forms",
+ "columnsFrom": [
+ "form_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.change_logs": {
+ "name": "change_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "changed_fields": {
+ "name": "changed_fields",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "old_values": {
+ "name": "old_values",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_values": {
+ "name": "new_values",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_name": {
+ "name": "user_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_synced": {
+ "name": "is_synced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "sync_attempts": {
+ "name": "sync_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_sync_error": {
+ "name": "last_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_systems": {
+ "name": "target_systems",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {
+ "idx_change_logs_contract_synced": {
+ "name": "idx_change_logs_contract_synced",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_synced",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_change_logs_created_at": {
+ "name": "idx_change_logs_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_change_logs_entity": {
+ "name": "idx_change_logs_entity",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_change_logs_sync_attempts": {
+ "name": "idx_change_logs_sync_attempts",
+ "columns": [
+ {
+ "expression": "sync_attempts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.document_attachments": {
+ "name": "document_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "document_attachments_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "revision_id": {
+ "name": "revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "upload_id": {
+ "name": "upload_id",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_id": {
+ "name": "file_id",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dolce_file_path": {
+ "name": "dolce_file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "document_attachments_revision_id_revisions_id_fk": {
+ "name": "document_attachments_revision_id_revisions_id_fk",
+ "tableFrom": "document_attachments",
+ "tableTo": "revisions",
+ "columnsFrom": [
+ "revision_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.documents": {
+ "name": "documents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "documents_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "pic": {
+ "name": "pic",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "doc_number": {
+ "name": "doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_doc_number": {
+ "name": "vendor_doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "issued_date": {
+ "name": "issued_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "drawing_kind": {
+ "name": "drawing_kind",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "drawing_move_gbn": {
+ "name": "drawing_move_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discipline": {
+ "name": "discipline",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_document_id": {
+ "name": "external_document_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_system_type": {
+ "name": "external_system_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_synced_at": {
+ "name": "external_synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "c_gbn": {
+ "name": "c_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "d_gbn": {
+ "name": "d_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "degree_gbn": {
+ "name": "degree_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dept_gbn": {
+ "name": "dept_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "j_gbn": {
+ "name": "j_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s_gbn": {
+ "name": "s_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shi_drawing_no": {
+ "name": "shi_drawing_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager": {
+ "name": "manager",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_enm": {
+ "name": "manager_enm",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_no": {
+ "name": "manager_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "register_group": {
+ "name": "register_group",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "register_group_id": {
+ "name": "register_group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "create_user_no": {
+ "name": "create_user_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "create_user_id": {
+ "name": "create_user_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "create_user_enm": {
+ "name": "create_user_enm",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "unique_contract_doc_status": {
+ "name": "unique_contract_doc_status",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "doc_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "unique_contract_vendor_doc": {
+ "name": "unique_contract_vendor_doc",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "vendor_doc_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"documents\".\"vendor_doc_number\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "unique_external_doc": {
+ "name": "unique_external_doc",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_system_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"documents\".\"external_document_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "drawing_kind_idx": {
+ "name": "drawing_kind_idx",
+ "columns": [
+ {
+ "expression": "drawing_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "documents_contract_id_contracts_id_fk": {
+ "name": "documents_contract_id_contracts_id_fk",
+ "tableFrom": "documents",
+ "tableTo": "contracts",
+ "columnsFrom": [
+ "contract_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.issue_stages": {
+ "name": "issue_stages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "issue_stages_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage_name": {
+ "name": "stage_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_date": {
+ "name": "plan_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actual_date": {
+ "name": "actual_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stage_status": {
+ "name": "stage_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PLANNED'"
+ },
+ "stage_order": {
+ "name": "stage_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'MEDIUM'"
+ },
+ "assignee_id": {
+ "name": "assignee_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "assignee_name": {
+ "name": "assignee_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reminder_days": {
+ "name": "reminder_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "unique_document_stage": {
+ "name": "unique_document_stage",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stage_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "document_stage_order": {
+ "name": "document_stage_order",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stage_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "issue_stages_document_id_documents_id_fk": {
+ "name": "issue_stages_document_id_documents_id_fk",
+ "tableFrom": "issue_stages",
+ "tableTo": "documents",
+ "columnsFrom": [
+ "document_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.revisions": {
+ "name": "revisions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "revisions_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "issue_stage_id": {
+ "name": "issue_stage_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploader_type": {
+ "name": "uploader_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'vendor'"
+ },
+ "uploader_id": {
+ "name": "uploader_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploader_name": {
+ "name": "uploader_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usage": {
+ "name": "usage",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usage_type": {
+ "name": "usage_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_status": {
+ "name": "revision_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'SUBMITTED'"
+ },
+ "submitted_date": {
+ "name": "submitted_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "review_start_date": {
+ "name": "review_start_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_date": {
+ "name": "approved_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejected_date": {
+ "name": "rejected_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewer_id": {
+ "name": "reviewer_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewer_name": {
+ "name": "reviewer_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "review_comments": {
+ "name": "review_comments",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_upload_id": {
+ "name": "external_upload_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment": {
+ "name": "comment",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "unique_stage_revision_usage": {
+ "name": "unique_stage_revision_usage",
+ "columns": [
+ {
+ "expression": "issue_stage_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "revision",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "usage",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "COALESCE(\"usage_type\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sync_batches": {
+ "name": "sync_batches",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_system": {
+ "name": "target_system",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "batch_size": {
+ "name": "batch_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "change_log_ids": {
+ "name": "change_log_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "success_count": {
+ "name": "success_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "sync_metadata": {
+ "name": "sync_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_sync_batches_contract_system": {
+ "name": "idx_sync_batches_contract_system",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_system",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_sync_batches_status": {
+ "name": "idx_sync_batches_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_sync_batches_created_at": {
+ "name": "idx_sync_batches_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sync_configs": {
+ "name": "sync_configs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_system": {
+ "name": "target_system",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sync_enabled": {
+ "name": "sync_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "sync_interval_minutes": {
+ "name": "sync_interval_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 30
+ },
+ "last_successful_sync": {
+ "name": "last_successful_sync",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_attempt": {
+ "name": "last_sync_attempt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpoint_url": {
+ "name": "endpoint_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth_token": {
+ "name": "auth_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "api_version": {
+ "name": "api_version",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'v1'"
+ },
+ "max_batch_size": {
+ "name": "max_batch_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 100
+ },
+ "retry_max_attempts": {
+ "name": "retry_max_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_sync_configs_contract_system": {
+ "name": "idx_sync_configs_contract_system",
+ "columns": [
+ {
+ "expression": "contract_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_system",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sync_configs_contract_id_contracts_id_fk": {
+ "name": "sync_configs_contract_id_contracts_id_fk",
+ "tableFrom": "sync_configs",
+ "tableTo": "contracts",
+ "columnsFrom": [
+ "contract_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_attachments": {
+ "name": "vendor_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'GENERAL'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_attachments_vendor_id_vendors_id_fk": {
+ "name": "vendor_attachments_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_attachments",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_candidates": {
+ "name": "vendor_candidates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'COLLECTED'"
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_candidates_vendor_id_vendors_id_fk": {
+ "name": "vendor_candidates_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_candidates",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_contacts": {
+ "name": "vendor_contacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_name": {
+ "name": "contact_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_position": {
+ "name": "contact_position",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_primary": {
+ "name": "is_primary",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_contacts_vendor_id_vendors_id_fk": {
+ "name": "vendor_contacts_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_contacts",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_possible_items": {
+ "name": "vendor_possible_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_possible_items_vendor_id_vendors_id_fk": {
+ "name": "vendor_possible_items_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_possible_items",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "vendor_possible_items_item_code_items_item_code_fk": {
+ "name": "vendor_possible_items_item_code_items_item_code_fk",
+ "tableFrom": "vendor_possible_items",
+ "tableTo": "items",
+ "columnsFrom": [
+ "item_code"
+ ],
+ "columnsTo": [
+ "item_code"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_possible_materials": {
+ "name": "vendor_possible_materials",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_possible_materials_vendor_id_vendors_id_fk": {
+ "name": "vendor_possible_materials_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_possible_materials",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "vendor_possible_materials_item_code_materials_item_code_fk": {
+ "name": "vendor_possible_materials_item_code_materials_item_code_fk",
+ "tableFrom": "vendor_possible_materials",
+ "tableTo": "materials",
+ "columnsFrom": [
+ "item_code"
+ ],
+ "columnsTo": [
+ "item_code"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_types": {
+ "name": "vendor_types",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_ko": {
+ "name": "name_ko",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_en": {
+ "name": "name_en",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "vendor_types_code_unique": {
+ "name": "vendor_types_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendors": {
+ "name": "vendors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website": {
+ "name": "website",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING_REVIEW'"
+ },
+ "vendor_type_id": {
+ "name": "vendor_type_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_name": {
+ "name": "representative_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_birth": {
+ "name": "representative_birth",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_email": {
+ "name": "representative_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_phone": {
+ "name": "representative_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "corporate_registration_number": {
+ "name": "corporate_registration_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credit_agency": {
+ "name": "credit_agency",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credit_rating": {
+ "name": "credit_rating",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cash_flow_rating": {
+ "name": "cash_flow_rating",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "business_size": {
+ "name": "business_size",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendors_vendor_type_id_vendor_types_id_fk": {
+ "name": "vendors_vendor_type_id_vendor_types_id_fk",
+ "tableFrom": "vendors",
+ "tableTo": "vendor_types",
+ "columnsFrom": [
+ "vendor_type_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'todo'"
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bug'"
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'low'"
+ },
+ "archived": {
+ "name": "archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "current_timestamp"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tasks_code_unique": {
+ "name": "tasks_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_candidate_logs": {
+ "name": "vendor_candidate_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_candidate_id": {
+ "name": "vendor_candidate_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "old_status": {
+ "name": "old_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_status": {
+ "name": "new_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment": {
+ "name": "comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_candidate_logs_vendor_candidate_id_vendor_candidates_id_fk": {
+ "name": "vendor_candidate_logs_vendor_candidate_id_vendor_candidates_id_fk",
+ "tableFrom": "vendor_candidate_logs",
+ "tableTo": "vendor_candidates",
+ "columnsFrom": [
+ "vendor_candidate_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_candidate_logs_user_id_users_id_fk": {
+ "name": "vendor_candidate_logs_user_id_users_id_fk",
+ "tableFrom": "vendor_candidate_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendors_logs": {
+ "name": "vendors_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "old_status": {
+ "name": "old_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_status": {
+ "name": "new_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment": {
+ "name": "comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendors_logs_vendor_id_vendors_id_fk": {
+ "name": "vendors_logs_vendor_id_vendors_id_fk",
+ "tableFrom": "vendors_logs",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendors_logs_user_id_users_id_fk": {
+ "name": "vendors_logs_user_id_users_id_fk",
+ "tableFrom": "vendors_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.basic_contract": {
+ "name": "basic_contract",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "basic_contract_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_by": {
+ "name": "requested_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "basic_contract_template_id_basic_contract_templates_id_fk": {
+ "name": "basic_contract_template_id_basic_contract_templates_id_fk",
+ "tableFrom": "basic_contract",
+ "tableTo": "basic_contract_templates",
+ "columnsFrom": [
+ "template_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "basic_contract_vendor_id_vendors_id_fk": {
+ "name": "basic_contract_vendor_id_vendors_id_fk",
+ "tableFrom": "basic_contract",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "basic_contract_requested_by_users_id_fk": {
+ "name": "basic_contract_requested_by_users_id_fk",
+ "tableFrom": "basic_contract",
+ "tableTo": "users",
+ "columnsFrom": [
+ "requested_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.basic_contract_templates": {
+ "name": "basic_contract_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "basic_contract_templates_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "template_name": {
+ "name": "template_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "validity_period": {
+ "name": "validity_period",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.incoterms": {
+ "name": "incoterms",
+ "schema": "",
+ "columns": {
+ "code": {
+ "name": "code",
+ "type": "varchar(20)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "incoterms_created_by_users_id_fk": {
+ "name": "incoterms_created_by_users_id_fk",
+ "tableFrom": "incoterms",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payment_terms": {
+ "name": "payment_terms",
+ "schema": "",
+ "columns": {
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "payment_terms_created_by_users_id_fk": {
+ "name": "payment_terms_created_by_users_id_fk",
+ "tableFrom": "payment_terms",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pr_items": {
+ "name": "pr_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "procurement_rfqs_id": {
+ "name": "procurement_rfqs_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_item": {
+ "name": "rfq_item",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_item": {
+ "name": "pr_item",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_no": {
+ "name": "pr_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_code": {
+ "name": "material_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_category": {
+ "name": "material_category",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acc": {
+ "name": "acc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_description": {
+ "name": "material_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size": {
+ "name": "size",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "uom": {
+ "name": "uom",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gross_weight": {
+ "name": "gross_weight",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "gw_uom": {
+ "name": "gw_uom",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spec_no": {
+ "name": "spec_no",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spec_url": {
+ "name": "spec_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tracking_no": {
+ "name": "tracking_no",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "major_yn": {
+ "name": "major_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "project_def": {
+ "name": "project_def",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_sc": {
+ "name": "project_sc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_kl": {
+ "name": "project_kl",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_lc": {
+ "name": "project_lc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_dl": {
+ "name": "project_dl",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pr_items_procurement_rfqs_id_procurement_rfqs_id_fk": {
+ "name": "pr_items_procurement_rfqs_id_procurement_rfqs_id_fk",
+ "tableFrom": "pr_items",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "procurement_rfqs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_attachments": {
+ "name": "procurement_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "procurement_rfqs_id": {
+ "name": "procurement_rfqs_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "procurement_rfq_details_id": {
+ "name": "procurement_rfq_details_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_attachments_procurement_rfqs_id_procurement_rfqs_id_fk": {
+ "name": "procurement_attachments_procurement_rfqs_id_procurement_rfqs_id_fk",
+ "tableFrom": "procurement_attachments",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "procurement_rfqs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_attachments_procurement_rfq_details_id_procurement_rfq_details_id_fk": {
+ "name": "procurement_attachments_procurement_rfq_details_id_procurement_rfq_details_id_fk",
+ "tableFrom": "procurement_attachments",
+ "tableTo": "procurement_rfq_details",
+ "columnsFrom": [
+ "procurement_rfq_details_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_attachments_created_by_users_id_fk": {
+ "name": "procurement_attachments_created_by_users_id_fk",
+ "tableFrom": "procurement_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "attachment_type_check": {
+ "name": "attachment_type_check",
+ "value": "\"procurement_attachments\".\"procurement_rfqs_id\" IS NOT NULL OR \"procurement_attachments\".\"procurement_rfq_details_id\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.procurement_quotation_items": {
+ "name": "procurement_quotation_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "quotation_id": {
+ "name": "quotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_item_id": {
+ "name": "pr_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "material_code": {
+ "name": "material_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_description": {
+ "name": "material_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uom": {
+ "name": "uom",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_price": {
+ "name": "unit_price",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_price": {
+ "name": "total_price",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USD'"
+ },
+ "vendor_material_code": {
+ "name": "vendor_material_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_material_description": {
+ "name": "vendor_material_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lead_time_in_days": {
+ "name": "lead_time_in_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_rate": {
+ "name": "tax_rate",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_amount": {
+ "name": "tax_amount",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount_rate": {
+ "name": "discount_rate",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount_amount": {
+ "name": "discount_amount",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_alternative": {
+ "name": "is_alternative",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "is_recommended": {
+ "name": "is_recommended",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_quotation_items_quotation_id_procurement_vendor_quotations_id_fk": {
+ "name": "procurement_quotation_items_quotation_id_procurement_vendor_quotations_id_fk",
+ "tableFrom": "procurement_quotation_items",
+ "tableTo": "procurement_vendor_quotations",
+ "columnsFrom": [
+ "quotation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_quotation_items_pr_item_id_pr_items_id_fk": {
+ "name": "procurement_quotation_items_pr_item_id_pr_items_id_fk",
+ "tableFrom": "procurement_quotation_items",
+ "tableTo": "pr_items",
+ "columnsFrom": [
+ "pr_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_rfq_attachments": {
+ "name": "procurement_rfq_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quotation_id": {
+ "name": "quotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_vendor_upload": {
+ "name": "is_vendor_upload",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_rfq_attachments_rfq_id_procurement_rfqs_id_fk": {
+ "name": "procurement_rfq_attachments_rfq_id_procurement_rfqs_id_fk",
+ "tableFrom": "procurement_rfq_attachments",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_attachments_comment_id_procurement_rfq_comments_id_fk": {
+ "name": "procurement_rfq_attachments_comment_id_procurement_rfq_comments_id_fk",
+ "tableFrom": "procurement_rfq_attachments",
+ "tableTo": "procurement_rfq_comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_attachments_quotation_id_procurement_vendor_quotations_id_fk": {
+ "name": "procurement_rfq_attachments_quotation_id_procurement_vendor_quotations_id_fk",
+ "tableFrom": "procurement_rfq_attachments",
+ "tableTo": "procurement_vendor_quotations",
+ "columnsFrom": [
+ "quotation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_attachments_uploaded_by_users_id_fk": {
+ "name": "procurement_rfq_attachments_uploaded_by_users_id_fk",
+ "tableFrom": "procurement_rfq_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_attachments_vendor_id_vendors_id_fk": {
+ "name": "procurement_rfq_attachments_vendor_id_vendors_id_fk",
+ "tableFrom": "procurement_rfq_attachments",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_rfq_comments": {
+ "name": "procurement_rfq_comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_vendor_comment": {
+ "name": "is_vendor_comment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "parent_comment_id": {
+ "name": "parent_comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_rfq_comments_rfq_id_procurement_rfqs_id_fk": {
+ "name": "procurement_rfq_comments_rfq_id_procurement_rfqs_id_fk",
+ "tableFrom": "procurement_rfq_comments",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_comments_vendor_id_vendors_id_fk": {
+ "name": "procurement_rfq_comments_vendor_id_vendors_id_fk",
+ "tableFrom": "procurement_rfq_comments",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_comments_user_id_users_id_fk": {
+ "name": "procurement_rfq_comments_user_id_users_id_fk",
+ "tableFrom": "procurement_rfq_comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_comments_parent_comment_id_procurement_rfq_comments_id_fk": {
+ "name": "procurement_rfq_comments_parent_comment_id_procurement_rfq_comments_id_fk",
+ "tableFrom": "procurement_rfq_comments",
+ "tableTo": "procurement_rfq_comments",
+ "columnsFrom": [
+ "parent_comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_rfq_details": {
+ "name": "procurement_rfq_details",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "procurement_rfqs_id": {
+ "name": "procurement_rfqs_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendors_id": {
+ "name": "vendors_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USD'"
+ },
+ "payment_terms_code": {
+ "name": "payment_terms_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_detail": {
+ "name": "incoterms_detail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tax_code": {
+ "name": "tax_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'VV'"
+ },
+ "place_of_shipping": {
+ "name": "place_of_shipping",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "place_of_destination": {
+ "name": "place_of_destination",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_reason": {
+ "name": "cancel_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "material_price_related_yn": {
+ "name": "material_price_related_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_rfq_details_procurement_rfqs_id_procurement_rfqs_id_fk": {
+ "name": "procurement_rfq_details_procurement_rfqs_id_procurement_rfqs_id_fk",
+ "tableFrom": "procurement_rfq_details",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "procurement_rfqs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_details_vendors_id_vendors_id_fk": {
+ "name": "procurement_rfq_details_vendors_id_vendors_id_fk",
+ "tableFrom": "procurement_rfq_details",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendors_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_details_payment_terms_code_payment_terms_code_fk": {
+ "name": "procurement_rfq_details_payment_terms_code_payment_terms_code_fk",
+ "tableFrom": "procurement_rfq_details",
+ "tableTo": "payment_terms",
+ "columnsFrom": [
+ "payment_terms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_details_incoterms_code_incoterms_code_fk": {
+ "name": "procurement_rfq_details_incoterms_code_incoterms_code_fk",
+ "tableFrom": "procurement_rfq_details",
+ "tableTo": "incoterms",
+ "columnsFrom": [
+ "incoterms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfq_details_updated_by_users_id_fk": {
+ "name": "procurement_rfq_details_updated_by_users_id_fk",
+ "tableFrom": "procurement_rfq_details",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_rfqs": {
+ "name": "procurement_rfqs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series": {
+ "name": "series",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rfq_send_date": {
+ "name": "rfq_send_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'RFQ Created'"
+ },
+ "rfq_sealed_yn": {
+ "name": "rfq_sealed_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "pic_code": {
+ "name": "pic_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_by": {
+ "name": "sent_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_rfqs_project_id_projects_id_fk": {
+ "name": "procurement_rfqs_project_id_projects_id_fk",
+ "tableFrom": "procurement_rfqs",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfqs_sent_by_users_id_fk": {
+ "name": "procurement_rfqs_sent_by_users_id_fk",
+ "tableFrom": "procurement_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sent_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfqs_created_by_users_id_fk": {
+ "name": "procurement_rfqs_created_by_users_id_fk",
+ "tableFrom": "procurement_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_rfqs_updated_by_users_id_fk": {
+ "name": "procurement_rfqs_updated_by_users_id_fk",
+ "tableFrom": "procurement_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "procurement_rfqs_rfq_code_unique": {
+ "name": "procurement_rfqs_rfq_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "rfq_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.procurement_vendor_quotations": {
+ "name": "procurement_vendor_quotations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quotation_code": {
+ "name": "quotation_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quotation_version": {
+ "name": "quotation_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "total_items_count": {
+ "name": "total_items_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "sub_total": {
+ "name": "sub_total",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "tax_total": {
+ "name": "tax_total",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "discount_total": {
+ "name": "discount_total",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_price": {
+ "name": "total_price",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USD'"
+ },
+ "valid_until": {
+ "name": "valid_until",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "estimated_delivery_date": {
+ "name": "estimated_delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_terms_code": {
+ "name": "payment_terms_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_detail": {
+ "name": "incoterms_detail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Draft'"
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "procurement_vendor_quotations_rfq_id_procurement_rfqs_id_fk": {
+ "name": "procurement_vendor_quotations_rfq_id_procurement_rfqs_id_fk",
+ "tableFrom": "procurement_vendor_quotations",
+ "tableTo": "procurement_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "procurement_vendor_quotations_vendor_id_vendors_id_fk": {
+ "name": "procurement_vendor_quotations_vendor_id_vendors_id_fk",
+ "tableFrom": "procurement_vendor_quotations",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_vendor_quotations_payment_terms_code_payment_terms_code_fk": {
+ "name": "procurement_vendor_quotations_payment_terms_code_payment_terms_code_fk",
+ "tableFrom": "procurement_vendor_quotations",
+ "tableTo": "payment_terms",
+ "columnsFrom": [
+ "payment_terms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "procurement_vendor_quotations_incoterms_code_incoterms_code_fk": {
+ "name": "procurement_vendor_quotations_incoterms_code_incoterms_code_fk",
+ "tableFrom": "procurement_vendor_quotations",
+ "tableTo": "incoterms",
+ "columnsFrom": [
+ "incoterms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.preset_shares": {
+ "name": "preset_shares",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "preset_id": {
+ "name": "preset_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "shared_with_user_id": {
+ "name": "shared_with_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'read'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "preset_shares_preset_id_table_presets_id_fk": {
+ "name": "preset_shares_preset_id_table_presets_id_fk",
+ "tableFrom": "preset_shares",
+ "tableTo": "table_presets",
+ "columnsFrom": [
+ "preset_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_presets": {
+ "name": "table_presets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_shared": {
+ "name": "is_shared",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_attachments": {
+ "name": "tech_sales_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tech_sales_rfq_id": {
+ "name": "tech_sales_rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_attachments_tech_sales_rfq_id_tech_sales_rfqs_id_fk": {
+ "name": "tech_sales_attachments_tech_sales_rfq_id_tech_sales_rfqs_id_fk",
+ "tableFrom": "tech_sales_attachments",
+ "tableTo": "tech_sales_rfqs",
+ "columnsFrom": [
+ "tech_sales_rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_attachments_created_by_users_id_fk": {
+ "name": "tech_sales_attachments_created_by_users_id_fk",
+ "tableFrom": "tech_sales_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_rfq_comment_attachments": {
+ "name": "tech_sales_rfq_comment_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quotation_id": {
+ "name": "quotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_vendor_upload": {
+ "name": "is_vendor_upload",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_rfq_comment_attachments_rfq_id_tech_sales_rfqs_id_fk": {
+ "name": "tech_sales_rfq_comment_attachments_rfq_id_tech_sales_rfqs_id_fk",
+ "tableFrom": "tech_sales_rfq_comment_attachments",
+ "tableTo": "tech_sales_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comment_attachments_comment_id_tech_sales_rfq_comments_id_fk": {
+ "name": "tech_sales_rfq_comment_attachments_comment_id_tech_sales_rfq_comments_id_fk",
+ "tableFrom": "tech_sales_rfq_comment_attachments",
+ "tableTo": "tech_sales_rfq_comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comment_attachments_quotation_id_tech_sales_vendor_quotations_id_fk": {
+ "name": "tech_sales_rfq_comment_attachments_quotation_id_tech_sales_vendor_quotations_id_fk",
+ "tableFrom": "tech_sales_rfq_comment_attachments",
+ "tableTo": "tech_sales_vendor_quotations",
+ "columnsFrom": [
+ "quotation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comment_attachments_uploaded_by_users_id_fk": {
+ "name": "tech_sales_rfq_comment_attachments_uploaded_by_users_id_fk",
+ "tableFrom": "tech_sales_rfq_comment_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comment_attachments_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_sales_rfq_comment_attachments_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_sales_rfq_comment_attachments",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_rfq_comments": {
+ "name": "tech_sales_rfq_comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_vendor_comment": {
+ "name": "is_vendor_comment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "parent_comment_id": {
+ "name": "parent_comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_rfq_comments_rfq_id_tech_sales_rfqs_id_fk": {
+ "name": "tech_sales_rfq_comments_rfq_id_tech_sales_rfqs_id_fk",
+ "tableFrom": "tech_sales_rfq_comments",
+ "tableTo": "tech_sales_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comments_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_sales_rfq_comments_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_sales_rfq_comments",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comments_user_id_users_id_fk": {
+ "name": "tech_sales_rfq_comments_user_id_users_id_fk",
+ "tableFrom": "tech_sales_rfq_comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_comments_parent_comment_id_tech_sales_rfq_comments_id_fk": {
+ "name": "tech_sales_rfq_comments_parent_comment_id_tech_sales_rfq_comments_id_fk",
+ "tableFrom": "tech_sales_rfq_comments",
+ "tableTo": "tech_sales_rfq_comments",
+ "columnsFrom": [
+ "parent_comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_rfq_items": {
+ "name": "tech_sales_rfq_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_shipbuilding_id": {
+ "name": "item_shipbuilding_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_offshore_top_id": {
+ "name": "item_offshore_top_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_offshore_hull_id": {
+ "name": "item_offshore_hull_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_type": {
+ "name": "item_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_rfq_items_rfq_id_tech_sales_rfqs_id_fk": {
+ "name": "tech_sales_rfq_items_rfq_id_tech_sales_rfqs_id_fk",
+ "tableFrom": "tech_sales_rfq_items",
+ "tableTo": "tech_sales_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_items_item_shipbuilding_id_item_shipbuilding_id_fk": {
+ "name": "tech_sales_rfq_items_item_shipbuilding_id_item_shipbuilding_id_fk",
+ "tableFrom": "tech_sales_rfq_items",
+ "tableTo": "item_shipbuilding",
+ "columnsFrom": [
+ "item_shipbuilding_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_items_item_offshore_top_id_item_offshore_top_id_fk": {
+ "name": "tech_sales_rfq_items_item_offshore_top_id_item_offshore_top_id_fk",
+ "tableFrom": "tech_sales_rfq_items",
+ "tableTo": "item_offshore_top",
+ "columnsFrom": [
+ "item_offshore_top_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfq_items_item_offshore_hull_id_item_offshore_hull_id_fk": {
+ "name": "tech_sales_rfq_items_item_offshore_hull_id_item_offshore_hull_id_fk",
+ "tableFrom": "tech_sales_rfq_items",
+ "tableTo": "item_offshore_hull",
+ "columnsFrom": [
+ "item_offshore_hull_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_rfqs": {
+ "name": "tech_sales_rfqs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bidding_project_id": {
+ "name": "bidding_project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_code": {
+ "name": "material_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rfq_send_date": {
+ "name": "rfq_send_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'RFQ Created'"
+ },
+ "pic_code": {
+ "name": "pic_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_by": {
+ "name": "sent_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "cancel_reason": {
+ "name": "cancel_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'SHIP'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_rfqs_bidding_project_id_bidding_projects_id_fk": {
+ "name": "tech_sales_rfqs_bidding_project_id_bidding_projects_id_fk",
+ "tableFrom": "tech_sales_rfqs",
+ "tableTo": "bidding_projects",
+ "columnsFrom": [
+ "bidding_project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfqs_sent_by_users_id_fk": {
+ "name": "tech_sales_rfqs_sent_by_users_id_fk",
+ "tableFrom": "tech_sales_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sent_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfqs_created_by_users_id_fk": {
+ "name": "tech_sales_rfqs_created_by_users_id_fk",
+ "tableFrom": "tech_sales_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tech_sales_rfqs_updated_by_users_id_fk": {
+ "name": "tech_sales_rfqs_updated_by_users_id_fk",
+ "tableFrom": "tech_sales_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tech_sales_rfqs_rfq_code_unique": {
+ "name": "tech_sales_rfqs_rfq_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "rfq_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_sales_vendor_quotations": {
+ "name": "tech_sales_vendor_quotations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quotation_code": {
+ "name": "quotation_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quotation_version": {
+ "name": "quotation_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "total_price": {
+ "name": "total_price",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "valid_until": {
+ "name": "valid_until",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Draft'"
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_sales_vendor_quotations_rfq_id_tech_sales_rfqs_id_fk": {
+ "name": "tech_sales_vendor_quotations_rfq_id_tech_sales_rfqs_id_fk",
+ "tableFrom": "tech_sales_vendor_quotations",
+ "tableTo": "tech_sales_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tech_sales_vendor_quotations_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_sales_vendor_quotations_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_sales_vendor_quotations",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ocr_rotation_attempts": {
+ "name": "ocr_rotation_attempts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rotation": {
+ "name": "rotation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "numeric(5, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tables_found": {
+ "name": "tables_found",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "text_quality": {
+ "name": "text_quality",
+ "type": "numeric(5, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "keyword_count": {
+ "name": "keyword_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "score": {
+ "name": "score",
+ "type": "numeric(5, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extracted_rows_count": {
+ "name": "extracted_rows_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ocr_rotation_attempts_session_id_ocr_sessions_id_fk": {
+ "name": "ocr_rotation_attempts_session_id_ocr_sessions_id_fk",
+ "tableFrom": "ocr_rotation_attempts",
+ "tableTo": "ocr_sessions",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ocr_rows": {
+ "name": "ocr_rows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_index": {
+ "name": "row_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "report_no": {
+ "name": "report_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "no": {
+ "name": "no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "identification_no": {
+ "name": "identification_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag_no": {
+ "name": "tag_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "joint_no": {
+ "name": "joint_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "joint_type": {
+ "name": "joint_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "welding_date": {
+ "name": "welding_date",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "numeric(5, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_table": {
+ "name": "source_table",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_row": {
+ "name": "source_row",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_ocr_report_no_unique": {
+ "name": "idx_ocr_report_no_unique",
+ "columns": [
+ {
+ "expression": "report_no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "joint_no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "joint_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ocr_rows_table_id_ocr_tables_id_fk": {
+ "name": "ocr_rows_table_id_ocr_tables_id_fk",
+ "tableFrom": "ocr_rows",
+ "tableTo": "ocr_tables",
+ "columnsFrom": [
+ "table_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ocr_rows_session_id_ocr_sessions_id_fk": {
+ "name": "ocr_rows_session_id_ocr_sessions_id_fk",
+ "tableFrom": "ocr_rows",
+ "tableTo": "ocr_sessions",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ocr_rows_user_id_users_id_fk": {
+ "name": "ocr_rows_user_id_users_id_fk",
+ "tableFrom": "ocr_rows",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ocr_sessions": {
+ "name": "ocr_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processing_time": {
+ "name": "processing_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "best_rotation": {
+ "name": "best_rotation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tables": {
+ "name": "total_tables",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_rows": {
+ "name": "total_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "image_enhanced": {
+ "name": "image_enhanced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pdf_converted": {
+ "name": "pdf_converted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "success": {
+ "name": "success",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "warnings": {
+ "name": "warnings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ocr_tables": {
+ "name": "ocr_tables",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "table_index": {
+ "name": "table_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ocr_tables_session_id_ocr_sessions_id_fk": {
+ "name": "ocr_tables_session_id_ocr_sessions_id_fk",
+ "tableFrom": "ocr_tables",
+ "tableTo": "ocr_sessions",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.b_rfq_attachment_revisions": {
+ "name": "b_rfq_attachment_revisions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision_no": {
+ "name": "revision_no",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision_comment": {
+ "name": "revision_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_latest": {
+ "name": "is_latest",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "latest_revision_idx": {
+ "name": "latest_revision_idx",
+ "columns": [
+ {
+ "expression": "attachment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_latest",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"b_rfq_attachment_revisions\".\"is_latest\" = $1",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "attachment_revision_idx": {
+ "name": "attachment_revision_idx",
+ "columns": [
+ {
+ "expression": "attachment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "revision_no",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "b_rfq_attachment_revisions_attachment_id_b_rfq_attachments_id_fk": {
+ "name": "b_rfq_attachment_revisions_attachment_id_b_rfq_attachments_id_fk",
+ "tableFrom": "b_rfq_attachment_revisions",
+ "tableTo": "b_rfq_attachments",
+ "columnsFrom": [
+ "attachment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "b_rfq_attachment_revisions_created_by_users_id_fk": {
+ "name": "b_rfq_attachment_revisions_created_by_users_id_fk",
+ "tableFrom": "b_rfq_attachment_revisions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.b_rfqs": {
+ "name": "b_rfqs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'DRAFT'"
+ },
+ "pic_code": {
+ "name": "pic_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pic_name": {
+ "name": "pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "eng_pic_name": {
+ "name": "eng_pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_company": {
+ "name": "project_company",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_flag": {
+ "name": "project_flag",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_site": {
+ "name": "project_site",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_no": {
+ "name": "package_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_name": {
+ "name": "package_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "b_rfqs_project_id_projects_id_fk": {
+ "name": "b_rfqs_project_id_projects_id_fk",
+ "tableFrom": "b_rfqs",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "b_rfqs_created_by_users_id_fk": {
+ "name": "b_rfqs_created_by_users_id_fk",
+ "tableFrom": "b_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "b_rfqs_updated_by_users_id_fk": {
+ "name": "b_rfqs_updated_by_users_id_fk",
+ "tableFrom": "b_rfqs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "b_rfqs_rfq_code_unique": {
+ "name": "b_rfqs_rfq_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "rfq_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.b_rfq_attachments": {
+ "name": "b_rfq_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serial_no": {
+ "name": "serial_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "current_revision": {
+ "name": "current_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Rev.0'"
+ },
+ "latest_revision_id": {
+ "name": "latest_revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "b_rfq_attachments_rfq_id_b_rfqs_id_fk": {
+ "name": "b_rfq_attachments_rfq_id_b_rfqs_id_fk",
+ "tableFrom": "b_rfq_attachments",
+ "tableTo": "b_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "b_rfq_attachments_created_by_users_id_fk": {
+ "name": "b_rfq_attachments_created_by_users_id_fk",
+ "tableFrom": "b_rfq_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.final_rfq": {
+ "name": "final_rfq",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "final_rfq_status": {
+ "name": "final_rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'DRAFT'"
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "valid_date": {
+ "name": "valid_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc": {
+ "name": "gtc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc_valid_date": {
+ "name": "gtc_valid_date",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "classification": {
+ "name": "classification",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sparepart": {
+ "name": "sparepart",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "short_list": {
+ "name": "short_list",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "return_yn": {
+ "name": "return_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cp_request_yn": {
+ "name": "cp_request_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "prject_gtc_yn": {
+ "name": "prject_gtc_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "return_revision": {
+ "name": "return_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'KRW'"
+ },
+ "payment_terms_code": {
+ "name": "payment_terms_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_code": {
+ "name": "tax_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'VV'"
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "place_of_shipping": {
+ "name": "place_of_shipping",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "place_of_destination": {
+ "name": "place_of_destination",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "firsttime_yn": {
+ "name": "firsttime_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "material_price_related_yn": {
+ "name": "material_price_related_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_remark": {
+ "name": "vendor_remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "final_rfq_rfq_id_b_rfqs_id_fk": {
+ "name": "final_rfq_rfq_id_b_rfqs_id_fk",
+ "tableFrom": "final_rfq",
+ "tableTo": "b_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "final_rfq_vendor_id_vendors_id_fk": {
+ "name": "final_rfq_vendor_id_vendors_id_fk",
+ "tableFrom": "final_rfq",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "final_rfq_incoterms_code_incoterms_code_fk": {
+ "name": "final_rfq_incoterms_code_incoterms_code_fk",
+ "tableFrom": "final_rfq",
+ "tableTo": "incoterms",
+ "columnsFrom": [
+ "incoterms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "final_rfq_payment_terms_code_payment_terms_code_fk": {
+ "name": "final_rfq_payment_terms_code_payment_terms_code_fk",
+ "tableFrom": "final_rfq",
+ "tableTo": "payment_terms",
+ "columnsFrom": [
+ "payment_terms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.initial_rfq": {
+ "name": "initial_rfq",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "initial_rfq_status": {
+ "name": "initial_rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'DRAFT'"
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "valid_date": {
+ "name": "valid_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc": {
+ "name": "gtc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc_valid_date": {
+ "name": "gtc_valid_date",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "classification": {
+ "name": "classification",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sparepart": {
+ "name": "sparepart",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "short_list": {
+ "name": "short_list",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "return_yn": {
+ "name": "return_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cp_request_yn": {
+ "name": "cp_request_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "prject_gtc_yn": {
+ "name": "prject_gtc_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "return_revision": {
+ "name": "return_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "rfq_revision": {
+ "name": "rfq_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "initial_rfq_rfq_id_b_rfqs_id_fk": {
+ "name": "initial_rfq_rfq_id_b_rfqs_id_fk",
+ "tableFrom": "initial_rfq",
+ "tableTo": "b_rfqs",
+ "columnsFrom": [
+ "rfq_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "initial_rfq_vendor_id_vendors_id_fk": {
+ "name": "initial_rfq_vendor_id_vendors_id_fk",
+ "tableFrom": "initial_rfq",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "initial_rfq_incoterms_code_incoterms_code_fk": {
+ "name": "initial_rfq_incoterms_code_incoterms_code_fk",
+ "tableFrom": "initial_rfq",
+ "tableTo": "incoterms",
+ "columnsFrom": [
+ "incoterms_code"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_attachment_responses": {
+ "name": "vendor_attachment_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rfq_record_id": {
+ "name": "rfq_record_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'NOT_RESPONDED'"
+ },
+ "current_revision": {
+ "name": "current_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'Rev.0'"
+ },
+ "responded_revision": {
+ "name": "responded_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_comment": {
+ "name": "response_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_comment": {
+ "name": "vendor_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_request_comment": {
+ "name": "revision_request_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "responded_at": {
+ "name": "responded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_requested_at": {
+ "name": "revision_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "vendor_response_idx": {
+ "name": "vendor_response_idx",
+ "columns": [
+ {
+ "expression": "attachment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "vendor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "rfq_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "vendor_attachment_responses_attachment_id_b_rfq_attachments_id_fk": {
+ "name": "vendor_attachment_responses_attachment_id_b_rfq_attachments_id_fk",
+ "tableFrom": "vendor_attachment_responses",
+ "tableTo": "b_rfq_attachments",
+ "columnsFrom": [
+ "attachment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_attachment_responses_vendor_id_vendors_id_fk": {
+ "name": "vendor_attachment_responses_vendor_id_vendors_id_fk",
+ "tableFrom": "vendor_attachment_responses",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_response_attachments_b": {
+ "name": "vendor_response_attachments_b",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_response_id": {
+ "name": "vendor_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_response_attachments_b_vendor_response_id_vendor_attachment_responses_id_fk": {
+ "name": "vendor_response_attachments_b_vendor_response_id_vendor_attachment_responses_id_fk",
+ "tableFrom": "vendor_response_attachments_b",
+ "tableTo": "vendor_attachment_responses",
+ "columnsFrom": [
+ "vendor_response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_response_attachments_b_uploaded_by_users_id_fk": {
+ "name": "vendor_response_attachments_b_uploaded_by_users_id_fk",
+ "tableFrom": "vendor_response_attachments_b",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_response_history": {
+ "name": "vendor_response_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_response_id": {
+ "name": "vendor_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previous_status": {
+ "name": "previous_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_status": {
+ "name": "new_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment": {
+ "name": "comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_by": {
+ "name": "action_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_at": {
+ "name": "action_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_response_history_vendor_response_id_vendor_attachment_responses_id_fk": {
+ "name": "vendor_response_history_vendor_response_id_vendor_attachment_responses_id_fk",
+ "tableFrom": "vendor_response_history",
+ "tableTo": "vendor_attachment_responses",
+ "columnsFrom": [
+ "vendor_response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_response_history_action_by_users_id_fk": {
+ "name": "vendor_response_history_action_by_users_id_fk",
+ "tableFrom": "vendor_response_history",
+ "tableTo": "users",
+ "columnsFrom": [
+ "action_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_vendor_attachments": {
+ "name": "tech_vendor_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'GENERAL'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_vendor_attachments_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_vendor_attachments_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_vendor_attachments",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_vendor_candidates": {
+ "name": "tech_vendor_candidates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'COLLECTED'"
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_vendor_candidates_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_vendor_candidates_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_vendor_candidates",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_vendor_contacts": {
+ "name": "tech_vendor_contacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_name": {
+ "name": "contact_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_position": {
+ "name": "contact_position",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_primary": {
+ "name": "is_primary",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_vendor_contacts_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_vendor_contacts_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_vendor_contacts",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_vendor_possible_items": {
+ "name": "tech_vendor_possible_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tech_vendor_possible_items_vendor_id_tech_vendors_id_fk": {
+ "name": "tech_vendor_possible_items_vendor_id_tech_vendors_id_fk",
+ "tableFrom": "tech_vendor_possible_items",
+ "tableTo": "tech_vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "tech_vendor_possible_items_item_code_items_item_code_fk": {
+ "name": "tech_vendor_possible_items_item_code_items_item_code_fk",
+ "tableFrom": "tech_vendor_possible_items",
+ "tableTo": "items",
+ "columnsFrom": [
+ "item_code"
+ ],
+ "columnsTo": [
+ "item_code"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tech_vendors": {
+ "name": "tech_vendors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country_eng": {
+ "name": "country_eng",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country_fab": {
+ "name": "country_fab",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_name": {
+ "name": "agent_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_phone": {
+ "name": "agent_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_email": {
+ "name": "agent_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address": {
+ "name": "address",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website": {
+ "name": "website",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tech_vendor_type": {
+ "name": "tech_vendor_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "representative_name": {
+ "name": "representative_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_email": {
+ "name": "representative_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_phone": {
+ "name": "representative_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_birth": {
+ "name": "representative_birth",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.esg_answer_options": {
+ "name": "esg_answer_options",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "esg_evaluation_item_id": {
+ "name": "esg_evaluation_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer_text": {
+ "name": "answer_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "score": {
+ "name": "score",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_index": {
+ "name": "order_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "esg_answer_options_esg_evaluation_item_id_esg_evaluation_items_id_fk": {
+ "name": "esg_answer_options_esg_evaluation_item_id_esg_evaluation_items_id_fk",
+ "tableFrom": "esg_answer_options",
+ "tableTo": "esg_evaluation_items",
+ "columnsFrom": [
+ "esg_evaluation_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.esg_evaluation_items": {
+ "name": "esg_evaluation_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "esg_evaluation_id": {
+ "name": "esg_evaluation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_item": {
+ "name": "evaluation_item",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_item_description": {
+ "name": "evaluation_item_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "order_index": {
+ "name": "order_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "esg_evaluation_items_esg_evaluation_id_esg_evaluations_id_fk": {
+ "name": "esg_evaluation_items_esg_evaluation_id_esg_evaluations_id_fk",
+ "tableFrom": "esg_evaluation_items",
+ "tableTo": "esg_evaluations",
+ "columnsFrom": [
+ "esg_evaluation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.esg_evaluation_responses": {
+ "name": "esg_evaluation_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "submission_id": {
+ "name": "submission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "esg_evaluation_item_id": {
+ "name": "esg_evaluation_item_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "esg_answer_option_id": {
+ "name": "esg_answer_option_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "selected_score": {
+ "name": "selected_score",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "additional_comments": {
+ "name": "additional_comments",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "esg_evaluation_responses_submission_id_evaluation_submissions_id_fk": {
+ "name": "esg_evaluation_responses_submission_id_evaluation_submissions_id_fk",
+ "tableFrom": "esg_evaluation_responses",
+ "tableTo": "evaluation_submissions",
+ "columnsFrom": [
+ "submission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "esg_evaluation_responses_esg_evaluation_item_id_esg_evaluation_items_id_fk": {
+ "name": "esg_evaluation_responses_esg_evaluation_item_id_esg_evaluation_items_id_fk",
+ "tableFrom": "esg_evaluation_responses",
+ "tableTo": "esg_evaluation_items",
+ "columnsFrom": [
+ "esg_evaluation_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "esg_evaluation_responses_esg_answer_option_id_esg_answer_options_id_fk": {
+ "name": "esg_evaluation_responses_esg_answer_option_id_esg_answer_options_id_fk",
+ "tableFrom": "esg_evaluation_responses",
+ "tableTo": "esg_answer_options",
+ "columnsFrom": [
+ "esg_answer_option_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.esg_evaluations": {
+ "name": "esg_evaluations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serial_number": {
+ "name": "serial_number",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inspection_item": {
+ "name": "inspection_item",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "esg_evaluations_serial_number_unique": {
+ "name": "esg_evaluations_serial_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "serial_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.evaluation_submissions": {
+ "name": "evaluation_submissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "submission_id": {
+ "name": "submission_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_year": {
+ "name": "evaluation_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "evaluation_round": {
+ "name": "evaluation_round",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submission_status": {
+ "name": "submission_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_by": {
+ "name": "reviewed_by",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "review_comments": {
+ "name": "review_comments",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "average_esg_score": {
+ "name": "average_esg_score",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_general_items": {
+ "name": "total_general_items",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "completed_general_items": {
+ "name": "completed_general_items",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "total_esg_items": {
+ "name": "total_esg_items",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "completed_esg_items": {
+ "name": "completed_esg_items",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "evaluation_submissions_company_id_vendors_id_fk": {
+ "name": "evaluation_submissions_company_id_vendors_id_fk",
+ "tableFrom": "evaluation_submissions",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "evaluation_submissions_submission_id_unique": {
+ "name": "evaluation_submissions_submission_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "submission_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.general_evaluation_responses": {
+ "name": "general_evaluation_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "submission_id": {
+ "name": "submission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "general_evaluation_id": {
+ "name": "general_evaluation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_text": {
+ "name": "response_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "has_attachments": {
+ "name": "has_attachments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "review_comments": {
+ "name": "review_comments",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "general_evaluation_responses_submission_id_evaluation_submissions_id_fk": {
+ "name": "general_evaluation_responses_submission_id_evaluation_submissions_id_fk",
+ "tableFrom": "general_evaluation_responses",
+ "tableTo": "evaluation_submissions",
+ "columnsFrom": [
+ "submission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "general_evaluation_responses_general_evaluation_id_general_evaluations_id_fk": {
+ "name": "general_evaluation_responses_general_evaluation_id_general_evaluations_id_fk",
+ "tableFrom": "general_evaluation_responses",
+ "tableTo": "general_evaluations",
+ "columnsFrom": [
+ "general_evaluation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.general_evaluations": {
+ "name": "general_evaluations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serial_number": {
+ "name": "serial_number",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inspection_item": {
+ "name": "inspection_item",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remarks": {
+ "name": "remarks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "general_evaluations_serial_number_unique": {
+ "name": "general_evaluations_serial_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "serial_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vendor_evaluation_attachments": {
+ "name": "vendor_evaluation_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "file_id": {
+ "name": "file_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "submission_id": {
+ "name": "submission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "general_evaluation_response_id": {
+ "name": "general_evaluation_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stored_file_name": {
+ "name": "stored_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vendor_evaluation_attachments_submission_id_evaluation_submissions_id_fk": {
+ "name": "vendor_evaluation_attachments_submission_id_evaluation_submissions_id_fk",
+ "tableFrom": "vendor_evaluation_attachments",
+ "tableTo": "evaluation_submissions",
+ "columnsFrom": [
+ "submission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "vendor_evaluation_attachments_general_evaluation_response_id_general_evaluation_responses_id_fk": {
+ "name": "vendor_evaluation_attachments_general_evaluation_response_id_general_evaluation_responses_id_fk",
+ "tableFrom": "vendor_evaluation_attachments",
+ "tableTo": "general_evaluation_responses",
+ "columnsFrom": [
+ "general_evaluation_response_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "vendor_evaluation_attachments_file_id_unique": {
+ "name": "vendor_evaluation_attachments_file_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "file_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.evaluation_target_reviewers": {
+ "name": "evaluation_target_reviewers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "evaluation_target_id": {
+ "name": "evaluation_target_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department_code": {
+ "name": "department_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department_name_from": {
+ "name": "department_name_from",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewer_user_id": {
+ "name": "reviewer_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "assigned_at": {
+ "name": "assigned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "assigned_by": {
+ "name": "assigned_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "evaluation_target_reviewers_evaluation_target_id_evaluation_targets_id_fk": {
+ "name": "evaluation_target_reviewers_evaluation_target_id_evaluation_targets_id_fk",
+ "tableFrom": "evaluation_target_reviewers",
+ "tableTo": "evaluation_targets",
+ "columnsFrom": [
+ "evaluation_target_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "evaluation_target_reviewers_reviewer_user_id_users_id_fk": {
+ "name": "evaluation_target_reviewers_reviewer_user_id_users_id_fk",
+ "tableFrom": "evaluation_target_reviewers",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reviewer_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "evaluation_target_reviewers_assigned_by_users_id_fk": {
+ "name": "evaluation_target_reviewers_assigned_by_users_id_fk",
+ "tableFrom": "evaluation_target_reviewers",
+ "tableTo": "users",
+ "columnsFrom": [
+ "assigned_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "unique_target_department": {
+ "name": "unique_target_department",
+ "nullsNotDistinct": false,
+ "columns": [
+ "evaluation_target_id",
+ "department_code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.evaluation_target_reviews": {
+ "name": "evaluation_target_reviews",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "evaluation_target_id": {
+ "name": "evaluation_target_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reviewer_user_id": {
+ "name": "reviewer_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department_code": {
+ "name": "department_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_approved": {
+ "name": "is_approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "review_comment": {
+ "name": "review_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "evaluation_target_reviews_evaluation_target_id_evaluation_targets_id_fk": {
+ "name": "evaluation_target_reviews_evaluation_target_id_evaluation_targets_id_fk",
+ "tableFrom": "evaluation_target_reviews",
+ "tableTo": "evaluation_targets",
+ "columnsFrom": [
+ "evaluation_target_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "evaluation_target_reviews_reviewer_user_id_users_id_fk": {
+ "name": "evaluation_target_reviews_reviewer_user_id_users_id_fk",
+ "tableFrom": "evaluation_target_reviews",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reviewer_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "unique_target_reviewer": {
+ "name": "unique_target_reviewer",
+ "nullsNotDistinct": false,
+ "columns": [
+ "evaluation_target_id",
+ "reviewer_user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.evaluation_targets": {
+ "name": "evaluation_targets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "evaluation_year": {
+ "name": "evaluation_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "division": {
+ "name": "division",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domestic_foreign": {
+ "name": "domestic_foreign",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "material_type": {
+ "name": "material_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "admin_comment": {
+ "name": "admin_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "admin_user_id": {
+ "name": "admin_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consolidated_comment": {
+ "name": "consolidated_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consensus_status": {
+ "name": "consensus_status",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_at": {
+ "name": "confirmed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_by": {
+ "name": "confirmed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ld_claim_count": {
+ "name": "ld_claim_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "ld_claim_amount": {
+ "name": "ld_claim_amount",
+ "type": "numeric(15, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "ld_claim_currency": {
+ "name": "ld_claim_currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'KRW'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "evaluation_targets_vendor_id_vendors_id_fk": {
+ "name": "evaluation_targets_vendor_id_vendors_id_fk",
+ "tableFrom": "evaluation_targets",
+ "tableTo": "vendors",
+ "columnsFrom": [
+ "vendor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "evaluation_targets_admin_user_id_users_id_fk": {
+ "name": "evaluation_targets_admin_user_id_users_id_fk",
+ "tableFrom": "evaluation_targets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "admin_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "evaluation_targets_confirmed_by_users_id_fk": {
+ "name": "evaluation_targets_confirmed_by_users_id_fk",
+ "tableFrom": "evaluation_targets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "confirmed_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project_gtc_files": {
+ "name": "project_gtc_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "project_gtc_files_project_id_projects_id_fk": {
+ "name": "project_gtc_files_project_id_projects_id_fk",
+ "tableFrom": "project_gtc_files",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.user_domain": {
+ "name": "user_domain",
+ "schema": "public",
+ "values": [
+ "evcp",
+ "partners"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {
+ "public.contracts_detail_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "contracts_detail_view_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_no": {
+ "name": "contract_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contract_name": {
+ "name": "contract_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payment_terms": {
+ "name": "payment_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_terms": {
+ "name": "delivery_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_location": {
+ "name": "delivery_location",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'KRW'"
+ },
+ "total_amount": {
+ "name": "total_amount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount": {
+ "name": "discount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax": {
+ "name": "tax",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shipping_fee": {
+ "name": "shipping_fee",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "net_total": {
+ "name": "net_total",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "partial_shipping_allowed": {
+ "name": "partial_shipping_allowed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "partial_payment_allowed": {
+ "name": "partial_payment_allowed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "remarks": {
+ "name": "remarks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"contracts\".\"id\", \"contracts\".\"contract_no\", \"contracts\".\"contract_name\", \"contracts\".\"status\", \"contracts\".\"start_date\", \"contracts\".\"end_date\", \"contracts\".\"project_id\", \"projects\".\"code\", \"projects\".\"name\", \"contracts\".\"vendor_id\", \"vendors\".\"vendor_name\", \"contracts\".\"payment_terms\", \"contracts\".\"delivery_terms\", \"contracts\".\"delivery_date\", \"contracts\".\"delivery_location\", \"contracts\".\"currency\", \"contracts\".\"total_amount\", \"contracts\".\"discount\", \"contracts\".\"tax\", \"contracts\".\"shipping_fee\", \"contracts\".\"net_total\", \"contracts\".\"partial_shipping_allowed\", \"contracts\".\"partial_payment_allowed\", \"contracts\".\"remarks\", \"contracts\".\"version\", \"contracts\".\"created_at\", \"contracts\".\"updated_at\", EXISTS (\n SELECT 1 \n FROM \"contract_envelopes\" \n WHERE \"contract_envelopes\".\"contract_id\" = \"contracts\".\"id\"\n ) as \"has_signature\", COALESCE((\n SELECT json_agg(\n json_build_object(\n 'id', ci.id,\n 'itemId', ci.item_id,\n 'description', ci.description,\n 'quantity', ci.quantity,\n 'unitPrice', ci.unit_price,\n 'taxRate', ci.tax_rate,\n 'taxAmount', ci.tax_amount,\n 'totalLineAmount', ci.total_line_amount,\n 'remark', ci.remark,\n 'createdAt', ci.created_at,\n 'updatedAt', ci.updated_at\n )\n )\n FROM \"contract_items\" AS ci\n WHERE ci.contract_id = \"contracts\".\"id\"\n ), '[]') as \"items\", COALESCE((\n SELECT json_agg(\n json_build_object(\n 'id', ce.id,\n 'envelopeId', ce.envelope_id,\n 'documentId', ce.document_id,\n 'envelopeStatus', ce.envelope_status,\n 'fileName', ce.file_name,\n 'filePath', ce.file_path,\n 'createdAt', ce.created_at,\n 'updatedAt', ce.updated_at,\n 'signers', (\n SELECT json_agg(\n json_build_object(\n 'id', cs.id,\n 'vendorContactId', cs.vendor_contact_id,\n 'signerType', cs.signer_type,\n 'signerEmail', cs.signer_email,\n 'signerName', cs.signer_name,\n 'signerPosition', cs.signer_position,\n 'signerStatus', cs.signer_status,\n 'signedAt', cs.signed_at\n )\n )\n FROM \"contract_signers\" AS cs\n WHERE cs.envelope_id = ce.id\n )\n )\n )\n FROM \"contract_envelopes\" AS ce\n WHERE ce.contract_id = \"contracts\".\"id\"\n ), '[]') as \"envelopes\" from \"contracts\" left join \"projects\" on \"contracts\".\"project_id\" = \"projects\".\"id\" left join \"vendors\" on \"contracts\".\"vendor_id\" = \"vendors\".\"id\"",
+ "name": "contracts_detail_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.poa_detail_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "poa_detail_view_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "contract_no": {
+ "name": "contract_no",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "change_reason": {
+ "name": "change_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approval_status": {
+ "name": "approval_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'PENDING'"
+ },
+ "delivery_terms": {
+ "name": "delivery_terms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_location": {
+ "name": "delivery_location",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_amount": {
+ "name": "total_amount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discount": {
+ "name": "discount",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax": {
+ "name": "tax",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shipping_fee": {
+ "name": "shipping_fee",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "net_total": {
+ "name": "net_total",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"poa\".\"id\", \"poa\".\"contract_no\", \"contracts\".\"project_id\", \"contracts\".\"vendor_id\", \"poa\".\"change_reason\", \"poa\".\"approval_status\", \"contracts\".\"contract_name\" as \"original_contract_name\", \"contracts\".\"status\" as \"original_status\", \"contracts\".\"start_date\" as \"original_start_date\", \"contracts\".\"end_date\" as \"original_end_date\", \"poa\".\"delivery_terms\", \"poa\".\"delivery_date\", \"poa\".\"delivery_location\", \"poa\".\"currency\", \"poa\".\"total_amount\", \"poa\".\"discount\", \"poa\".\"tax\", \"poa\".\"shipping_fee\", \"poa\".\"net_total\", \"poa\".\"created_at\", \"poa\".\"updated_at\", EXISTS (\n SELECT 1 \n FROM \"contract_envelopes\" \n WHERE \"contract_envelopes\".\"contract_id\" = \"poa\".\"id\"\n ) as \"has_signature\" from \"poa\" left join \"contracts\" on \"poa\".\"contract_no\" = \"contracts\".\"contract_no\"",
+ "name": "poa_detail_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.project_approved_vendors": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING_REVIEW'"
+ },
+ "name_ko": {
+ "name": "name_ko",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_en": {
+ "name": "name_en",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ship'"
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "select \"vendors\".\"id\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", \"vendors\".\"tax_id\", \"vendors\".\"email\", \"vendors\".\"phone\", \"vendors\".\"status\", \"vendor_types\".\"name_ko\", \"vendor_types\".\"name_en\", \"projects\".\"code\", \"projects\".\"name\", \"projects\".\"type\", \"vendor_pq_submissions\".\"submitted_at\", \"vendor_pq_submissions\".\"approved_at\" from \"vendors\" inner join \"vendor_pq_submissions\" on \"vendor_pq_submissions\".\"vendor_id\" = \"vendors\".\"id\" inner join \"projects\" on \"vendor_pq_submissions\".\"project_id\" = \"projects\".\"id\" left join \"vendor_types\" on \"vendors\".\"vendor_type_id\" = \"vendor_types\".\"id\" where \"vendor_pq_submissions\".\"status\" = 'APPROVED'",
+ "name": "project_approved_vendors",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_investigations_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pq_submission_id": {
+ "name": "pq_submission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requester_id": {
+ "name": "requester_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "qm_manager_id": {
+ "name": "qm_manager_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_status": {
+ "name": "investigation_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PLANNED'"
+ },
+ "evaluation_type": {
+ "name": "evaluation_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_address": {
+ "name": "investigation_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_method": {
+ "name": "investigation_method",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduled_start_at": {
+ "name": "scheduled_start_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduled_end_at": {
+ "name": "scheduled_end_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forecasted_at": {
+ "name": "forecasted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_at": {
+ "name": "confirmed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "evaluation_score": {
+ "name": "evaluation_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "evaluation_result": {
+ "name": "evaluation_result",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_notes": {
+ "name": "investigation_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "select \"vendor_investigations\".\"id\", \"vendor_investigations\".\"vendor_id\", \"vendor_investigations\".\"pq_submission_id\", \"vendor_investigations\".\"requester_id\", \"vendor_investigations\".\"qm_manager_id\", \"vendor_investigations\".\"investigation_status\", \"vendor_investigations\".\"evaluation_type\", \"vendor_investigations\".\"investigation_address\", \"vendor_investigations\".\"investigation_method\", \"vendor_investigations\".\"scheduled_start_at\", \"vendor_investigations\".\"scheduled_end_at\", \"vendor_investigations\".\"forecasted_at\", \"vendor_investigations\".\"requested_at\", \"vendor_investigations\".\"confirmed_at\", \"vendor_investigations\".\"completed_at\", \"vendor_investigations\".\"evaluation_score\", \"vendor_investigations\".\"evaluation_result\", \"vendor_investigations\".\"investigation_notes\", \"vendor_investigations\".\"created_at\", \"vendor_investigations\".\"updated_at\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", requester.name as \"requesterName\", requester.email as \"requesterEmail\", qm_manager.name as \"qmManagerName\", qm_manager.email as \"qmManagerEmail\", (\n CASE \n WHEN EXISTS (\n SELECT 1 FROM vendor_investigation_attachments via \n WHERE via.investigation_id = \"vendor_investigations\".\"id\"\n ) \n THEN true \n ELSE false \n END\n ) as \"hasAttachments\" from \"vendor_investigations\" left join \"vendors\" on \"vendor_investigations\".\"vendor_id\" = \"vendors\".\"id\" left join users AS requester on \"vendor_investigations\".\"requester_id\" = requester.id left join users AS qm_manager on \"vendor_investigations\".\"qm_manager_id\" = qm_manager.id",
+ "name": "vendor_investigations_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.cbe_view": {
+ "columns": {},
+ "definition": "select \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"rfq_id\" as \"rfq_id\", \"cbe_evaluations\".\"vendor_id\" as \"vendor_id\", \"cbe_evaluations\".\"total_cost\" as \"total_cost\", \"cbe_evaluations\".\"currency\" as \"currency\", \"cbe_evaluations\".\"payment_terms\" as \"payment_terms\", \"cbe_evaluations\".\"incoterms\" as \"incoterms\", \"cbe_evaluations\".\"result\" as \"result\", \"cbe_evaluations\".\"notes\" as \"notes\", \"cbe_evaluations\".\"evaluated_by\" as \"evaluated_by\", \"cbe_evaluations\".\"evaluated_at\" as \"evaluated_at\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"users\".\"name\" as \"evaluator_name\", \"users\".\"email\" as \"evaluator_email\" from \"cbe_evaluations\" inner join \"rfqs\" on \"cbe_evaluations\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"cbe_evaluations\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" on \"cbe_evaluations\".\"evaluated_by\" = \"users\".\"id\"",
+ "name": "cbe_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.rfqs_view": {
+ "columns": {},
+ "definition": "select \"rfqs\".\"id\" as \"rfq_id\", \"rfqs\".\"status\" as \"status\", \"rfqs\".\"created_at\" as \"created_at\", \"rfqs\".\"updated_at\" as \"updated_at\", \"rfqs\".\"created_by\" as \"created_by\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"rfqs\".\"parent_rfq_id\" as \"parent_rfq_id\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"users\".\"email\" as \"user_email\", \"users\".\"name\" as \"user_name\", (\n SELECT COUNT(*) \n FROM \"rfq_items\" \n WHERE \"rfq_items\".\"rfq_id\" = \"rfqs\".\"id\"\n ) as \"item_count\", (\n SELECT COUNT(*) \n FROM \"rfq_attachments\" \n WHERE \"rfq_attachments\".\"rfq_id\" = \"rfqs\".\"id\"\n ) as \"attachment_count\" from \"rfqs\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" on \"rfqs\".\"created_by\" = \"users\".\"id\"",
+ "name": "rfqs_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_cbe_view": {
+ "columns": {},
+ "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"id\" as \"vendor_response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"result\" as \"cbe_result\", \"cbe_evaluations\".\"notes\" as \"cbe_note\", \"cbe_evaluations\".\"updated_at\" as \"cbe_updated\", \"cbe_evaluations\".\"total_cost\" as \"total_cost\", \"cbe_evaluations\".\"currency\" as \"currency\", \"cbe_evaluations\".\"payment_terms\" as \"payment_terms\", \"cbe_evaluations\".\"incoterms\" as \"incoterms\", \"cbe_evaluations\".\"delivery_schedule\" as \"delivery_schedule\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"cbe_evaluations\" on (\"cbe_evaluations\".\"vendor_id\" = \"vendors\".\"id\" and \"cbe_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\")",
+ "name": "vendor_cbe_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_response_cbe_view": {
+ "columns": {},
+ "definition": "select \"vendor_responses\".\"id\" as \"response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"vendor_id\" as \"vendor_id\", \"vendor_responses\".\"response_status\" as \"response_status\", \"vendor_responses\".\"notes\" as \"response_notes\", \"vendor_responses\".\"responded_by\" as \"responded_by\", \"vendor_responses\".\"responded_at\" as \"responded_at\", \"vendor_responses\".\"updated_at\" as \"response_updated_at\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"rfqs\".\"due_date\" as \"rfq_due_date\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"status\" as \"vendor_status\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"vendor_commercial_responses\".\"id\" as \"commercial_response_id\", \"vendor_commercial_responses\".\"response_status\" as \"commercial_response_status\", \"vendor_commercial_responses\".\"total_price\" as \"total_price\", \"vendor_commercial_responses\".\"currency\" as \"currency\", \"vendor_commercial_responses\".\"payment_terms\" as \"payment_terms\", \"vendor_commercial_responses\".\"incoterms\" as \"incoterms\", \"vendor_commercial_responses\".\"delivery_period\" as \"delivery_period\", \"vendor_commercial_responses\".\"warranty_period\" as \"warranty_period\", \"vendor_commercial_responses\".\"validity_period\" as \"validity_period\", \"vendor_commercial_responses\".\"price_breakdown\" as \"price_breakdown\", \"vendor_commercial_responses\".\"commercial_notes\" as \"commercial_notes\", \"vendor_commercial_responses\".\"created_at\" as \"commercial_created_at\", \"vendor_commercial_responses\".\"updated_at\" as \"commercial_updated_at\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"attachment_count\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"commercial_response_id\" = \"vendor_commercial_responses\".\"id\"\n ) as \"commercial_attachment_count\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n AND \"vendor_response_attachments\".\"attachment_type\" = 'TECHNICAL_SPEC'\n ) as \"technical_attachment_count\", (\n SELECT MAX(\"uploaded_at\") \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"latest_attachment_date\" from \"vendor_responses\" inner join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_commercial_responses\" on \"vendor_commercial_responses\".\"response_id\" = \"vendor_responses\".\"id\"",
+ "name": "vendor_response_cbe_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_responses_view": {
+ "columns": {},
+ "definition": "select \"vendor_responses\".\"id\" as \"response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"vendor_id\" as \"vendor_id\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"rfqs\".\"due_date\" as \"rfq_due_date\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"created_at\" as \"rfq_created_at\", \"rfqs\".\"updated_at\" as \"rfq_updated_at\", \"rfqs\".\"created_by\" as \"rfq_created_by\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendor_responses\".\"response_status\" as \"response_status\", \"vendor_responses\".\"responded_at\" as \"responded_at\", CASE WHEN \"vendor_technical_responses\".\"id\" IS NOT NULL THEN TRUE ELSE FALSE END as \"has_technical_response\", \"vendor_technical_responses\".\"id\" as \"technical_response_id\", CASE WHEN \"vendor_commercial_responses\".\"id\" IS NOT NULL THEN TRUE ELSE FALSE END as \"has_commercial_response\", \"vendor_commercial_responses\".\"id\" as \"commercial_response_id\", \"vendor_commercial_responses\".\"total_price\" as \"total_price\", \"vendor_commercial_responses\".\"currency\" as \"currency\", \"rfq_evaluations\".\"id\" as \"tbe_id\", \"rfq_evaluations\".\"result\" as \"tbe_result\", \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"result\" as \"cbe_result\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"attachment_count\" from \"vendor_responses\" inner join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_technical_responses\" on \"vendor_technical_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"vendor_commercial_responses\" on \"vendor_commercial_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"rfq_evaluations\" on (\"rfq_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\" and \"rfq_evaluations\".\"vendor_id\" = \"vendor_responses\".\"vendor_id\" and \"rfq_evaluations\".\"eval_type\" = 'TBE') left join \"cbe_evaluations\" on (\"cbe_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\" and \"cbe_evaluations\".\"vendor_id\" = \"vendor_responses\".\"vendor_id\")",
+ "name": "vendor_responses_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_rfq_view": {
+ "columns": {},
+ "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\"",
+ "name": "vendor_rfq_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_tbe_view": {
+ "columns": {},
+ "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"id\" as \"vendor_response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"vendor_technical_responses\".\"id\" as \"technical_response_id\", \"vendor_technical_responses\".\"response_status\" as \"technical_response_status\", \"vendor_technical_responses\".\"summary\" as \"technical_summary\", \"vendor_technical_responses\".\"notes\" as \"technical_notes\", \"vendor_technical_responses\".\"updated_at\" as \"technical_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"rfq_evaluations\".\"id\" as \"tbe_id\", \"rfq_evaluations\".\"result\" as \"tbe_result\", \"rfq_evaluations\".\"notes\" as \"tbe_note\", \"rfq_evaluations\".\"updated_at\" as \"tbe_updated\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_technical_responses\" on \"vendor_technical_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"rfq_evaluations\" on (\"rfq_evaluations\".\"vendor_id\" = \"vendors\".\"id\" and \"rfq_evaluations\".\"eval_type\" = 'TBE' and \"rfq_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\")",
+ "name": "vendor_tbe_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.role_view": {
+ "columns": {},
+ "definition": "select \"roles\".\"id\" as \"id\", \"roles\".\"name\" as \"name\", \"roles\".\"description\" as \"description\", \"roles\".\"domain\" as \"domain\", \"roles\".\"created_at\" as \"created_at\", \"vendors\".\"id\" as \"company_id\", \"vendors\".\"vendor_name\" as \"company_name\", COUNT(\"users\".\"id\") as \"user_count\" from \"roles\" left join \"user_roles\" on \"user_roles\".\"role_id\" = \"roles\".\"id\" left join \"users\" on \"users\".\"id\" = \"user_roles\".\"user_id\" left join \"vendors\" on \"roles\".\"company_id\" = \"vendors\".\"id\" group by \"roles\".\"id\", \"vendors\".\"id\"",
+ "name": "role_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.user_view": {
+ "columns": {},
+ "definition": "select \"users\".\"id\" as \"user_id\", \"users\".\"name\" as \"user_name\", \"users\".\"email\" as \"user_email\", \"users\".\"domain\" as \"user_domain\", \"users\".\"image_url\" as \"user_image\", \"vendors\".\"id\" as \"company_id\", \"vendors\".\"vendor_name\" as \"company_name\", \n array_agg(\"roles\".\"name\")\n as \"roles\", \"users\".\"created_at\" as \"created_at\" from \"users\" left join \"vendors\" on \"users\".\"company_id\" = \"vendors\".\"id\" left join \"user_roles\" on \"users\".\"id\" = \"user_roles\".\"user_id\" left join \"roles\" on \"user_roles\".\"role_id\" = \"roles\".\"id\" group by \"users\".\"id\", \"vendors\".\"id\"",
+ "name": "user_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.form_lists_view": {
+ "columns": {},
+ "definition": "select \"tag_type_class_form_mappings\".\"id\" as \"id\", \"tag_type_class_form_mappings\".\"project_id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"tag_type_class_form_mappings\".\"tag_type_label\" as \"tag_type_label\", \"tag_type_class_form_mappings\".\"class_label\" as \"class_label\", \"tag_type_class_form_mappings\".\"form_code\" as \"form_code\", \"tag_type_class_form_mappings\".\"form_name\" as \"form_name\", \"tag_type_class_form_mappings\".\"ep\" as \"ep\", \"tag_type_class_form_mappings\".\"remark\" as \"remark\", \"tag_type_class_form_mappings\".\"created_at\" as \"created_at\", \"tag_type_class_form_mappings\".\"updated_at\" as \"updated_at\" from \"tag_type_class_form_mappings\" inner join \"projects\" on \"tag_type_class_form_mappings\".\"project_id\" = \"projects\".\"id\"",
+ "name": "form_lists_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.view_tag_subfields": {
+ "columns": {
+ "tag_type_code": {
+ "name": "tag_type_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes_id": {
+ "name": "attributes_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes_description": {
+ "name": "attributes_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expression": {
+ "name": "expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delimiter": {
+ "name": "delimiter",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "definition": "select \"tag_subfields\".\"id\" as \"id\", \"tag_subfields\".\"tag_type_code\", \"tag_types\".\"description\", \"tag_subfields\".\"attributes_id\", \"tag_subfields\".\"attributes_description\", \"tag_subfields\".\"expression\", \"tag_subfields\".\"delimiter\", \"tag_subfields\".\"sort_order\", \"tag_subfields\".\"created_at\", \"tag_subfields\".\"updated_at\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\", \"projects\".\"name\" from \"tag_subfields\" inner join \"tag_types\" on (\"tag_subfields\".\"tag_type_code\" = \"tag_types\".\"code\" and \"tag_subfields\".\"project_id\" = \"tag_types\".\"project_id\") inner join \"projects\" on \"tag_subfields\".\"project_id\" = \"projects\".\"id\"",
+ "name": "view_tag_subfields",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.document_stages_view": {
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "doc_number": {
+ "name": "doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issued_date": {
+ "name": "issued_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage_count": {
+ "name": "stage_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage_list": {
+ "name": "stage_list",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "definition": "\n SELECT\n d.id AS document_id,\n d.doc_number,\n d.title,\n d.status,\n d.issued_date,\n d.contract_id,\n (SELECT COUNT(*) FROM issue_stages WHERE document_id = d.id) AS stage_count,\n COALESCE( \n (SELECT json_agg(i.stage_name) FROM issue_stages i WHERE i.document_id = d.id), \n '[]'\n ) AS stage_list,\n d.created_at,\n d.updated_at\n FROM documents d\n",
+ "name": "document_stages_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.enhanced_documents_view": {
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "doc_number": {
+ "name": "doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "drawing_kind": {
+ "name": "drawing_kind",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_doc_number": {
+ "name": "vendor_doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pic": {
+ "name": "pic",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issued_date": {
+ "name": "issued_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_code": {
+ "name": "project_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "c_gbn": {
+ "name": "c_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "d_gbn": {
+ "name": "d_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "degree_gbn": {
+ "name": "degree_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dept_gbn": {
+ "name": "dept_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "j_gbn": {
+ "name": "j_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s_gbn": {
+ "name": "s_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_id": {
+ "name": "current_stage_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_name": {
+ "name": "current_stage_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_status": {
+ "name": "current_stage_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_order": {
+ "name": "current_stage_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_plan_date": {
+ "name": "current_stage_plan_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_actual_date": {
+ "name": "current_stage_actual_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_assignee_name": {
+ "name": "current_stage_assignee_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_stage_priority": {
+ "name": "current_stage_priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "days_until_due": {
+ "name": "days_until_due",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_overdue": {
+ "name": "is_overdue",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "days_difference": {
+ "name": "days_difference",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_stages": {
+ "name": "total_stages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_stages": {
+ "name": "completed_stages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "progress_percentage": {
+ "name": "progress_percentage",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_id": {
+ "name": "latest_revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision": {
+ "name": "latest_revision",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_status": {
+ "name": "latest_revision_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_uploader_name": {
+ "name": "latest_revision_uploader_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_submitted_date": {
+ "name": "latest_submitted_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "all_stages": {
+ "name": "all_stages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_count": {
+ "name": "attachment_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "definition": "\n WITH document_stats AS (\n SELECT \n d.id as document_id,\n COUNT(ist.id) as total_stages,\n COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) as completed_stages,\n CASE \n WHEN COUNT(ist.id) > 0 \n THEN ROUND((COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) * 100.0) / COUNT(ist.id))\n ELSE 0 \n END as progress_percentage\n FROM documents d\n LEFT JOIN issue_stages ist ON d.id = ist.document_id\n GROUP BY d.id\n ),\n current_stage_info AS (\n SELECT DISTINCT ON (document_id)\n document_id,\n id as current_stage_id,\n stage_name as current_stage_name,\n stage_status as current_stage_status,\n stage_order as current_stage_order,\n plan_date as current_stage_plan_date,\n actual_date as current_stage_actual_date,\n assignee_name as current_stage_assignee_name,\n priority as current_stage_priority,\n CASE \n WHEN actual_date IS NULL AND plan_date IS NOT NULL \n THEN plan_date - CURRENT_DATE\n ELSE NULL \n END as days_until_due,\n CASE \n WHEN actual_date IS NULL AND plan_date < CURRENT_DATE \n THEN true\n WHEN actual_date IS NOT NULL AND actual_date > plan_date \n THEN true\n ELSE false \n END as is_overdue,\n CASE \n WHEN actual_date IS NOT NULL AND plan_date IS NOT NULL \n THEN actual_date - plan_date\n ELSE NULL \n END as days_difference\n FROM issue_stages\n WHERE stage_status NOT IN ('COMPLETED', 'APPROVED')\n ORDER BY document_id, stage_order ASC, priority DESC\n ),\n latest_revision_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n r.id as latest_revision_id,\n r.revision as latest_revision,\n r.revision_status as latest_revision_status,\n r.uploader_name as latest_revision_uploader_name,\n r.submitted_date as latest_submitted_date\n FROM revisions r\n JOIN issue_stages ist ON r.issue_stage_id = ist.id\n ORDER BY ist.document_id, r.created_at DESC\n ),\n -- 리비전별 첨부파일 집계\n revision_attachments AS (\n SELECT \n r.id as revision_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', da.id,\n 'revisionId', da.revision_id,\n 'fileName', da.file_name,\n 'filePath', da.file_path,\n 'fileSize', da.file_size,\n 'fileType', da.file_type,\n 'createdAt', da.created_at,\n 'updatedAt', da.updated_at\n ) ORDER BY da.created_at\n ) FILTER (WHERE da.id IS NOT NULL),\n '[]'::json\n ) as attachments\n FROM revisions r\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY r.id\n ),\n -- 스테이지별 리비전 집계 (첨부파일 포함)\n stage_revisions AS (\n SELECT \n ist.id as stage_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', r.id,\n 'issueStageId', r.issue_stage_id,\n 'revision', r.revision,\n 'uploaderType', r.uploader_type,\n 'uploaderId', r.uploader_id,\n 'uploaderName', r.uploader_name,\n 'comment', r.comment,\n 'usage', r.usage,\n 'revisionStatus', r.revision_status,\n 'submittedDate', r.submitted_date,\n 'uploadedAt', r.uploaded_at,\n 'approvedDate', r.approved_date,\n 'reviewStartDate', r.review_start_date,\n 'rejectedDate', r.rejected_date,\n 'reviewerId', r.reviewer_id,\n 'reviewerName', r.reviewer_name,\n 'reviewComments', r.review_comments,\n 'createdAt', r.created_at,\n 'updatedAt', r.updated_at,\n 'attachments', ra.attachments\n ) ORDER BY r.created_at\n ) FILTER (WHERE r.id IS NOT NULL),\n '[]'::json\n ) as revisions\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN revision_attachments ra ON r.id = ra.revision_id\n GROUP BY ist.id\n ),\n -- 문서별 스테이지 집계 (리비전 포함)\n stage_aggregation AS (\n SELECT \n ist.document_id,\n json_agg(\n json_build_object(\n 'id', ist.id,\n 'stageName', ist.stage_name,\n 'stageStatus', ist.stage_status,\n 'stageOrder', ist.stage_order,\n 'planDate', ist.plan_date,\n 'actualDate', ist.actual_date,\n 'assigneeName', ist.assignee_name,\n 'priority', ist.priority,\n 'revisions', sr.revisions\n ) ORDER BY ist.stage_order\n ) as all_stages\n FROM issue_stages ist\n LEFT JOIN stage_revisions sr ON ist.id = sr.stage_id\n GROUP BY ist.document_id\n ),\n attachment_counts AS (\n SELECT \n ist.document_id,\n COUNT(da.id) as attachment_count\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY ist.document_id\n )\n \n SELECT \n d.id as document_id,\n d.doc_number,\n d.drawing_kind,\n d.vendor_doc_number, -- ✅ 벤더 문서 번호 추가\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n\n d.c_gbn,\n d.d_gbn,\n d.degree_gbn,\n d.dept_gbn,\n d.s_gbn,\n d.j_gbn,\n\n\n \n -- ✅ 프로젝트 및 벤더 정보 추가\n p.code as project_code,\n v.vendor_name as vendor_name,\n v.vendor_code as vendor_code,\n \n -- 현재 스테이지 정보\n csi.current_stage_id,\n csi.current_stage_name,\n csi.current_stage_status,\n csi.current_stage_order,\n csi.current_stage_plan_date,\n csi.current_stage_actual_date,\n csi.current_stage_assignee_name,\n csi.current_stage_priority,\n \n -- 계산 필드\n csi.days_until_due,\n csi.is_overdue,\n csi.days_difference,\n \n -- 진행률 정보\n ds.total_stages,\n ds.completed_stages,\n ds.progress_percentage,\n \n -- 최신 리비전 정보\n lri.latest_revision_id,\n lri.latest_revision,\n lri.latest_revision_status,\n lri.latest_revision_uploader_name,\n lri.latest_submitted_date,\n \n -- 전체 스테이지 (리비전 및 첨부파일 포함)\n COALESCE(sa.all_stages, '[]'::json) as all_stages,\n \n -- 기타\n COALESCE(ac.attachment_count, 0) as attachment_count,\n d.created_at,\n d.updated_at\n \n FROM documents d\n -- ✅ contracts, projects, vendors 테이블 JOIN 추가\n LEFT JOIN contracts c ON d.contract_id = c.id\n LEFT JOIN projects p ON c.project_id = p.id\n LEFT JOIN vendors v ON c.vendor_id = v.id\n \n LEFT JOIN document_stats ds ON d.id = ds.document_id\n LEFT JOIN current_stage_info csi ON d.id = csi.document_id\n LEFT JOIN latest_revision_info lri ON d.id = lri.document_id\n LEFT JOIN stage_aggregation sa ON d.id = sa.document_id\n LEFT JOIN attachment_counts ac ON d.id = ac.document_id\n \n ORDER BY d.created_at DESC\n",
+ "name": "enhanced_documents_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.simplified_documents_view": {
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "doc_number": {
+ "name": "doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "drawing_kind": {
+ "name": "drawing_kind",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_doc_number": {
+ "name": "vendor_doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pic": {
+ "name": "pic",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issued_date": {
+ "name": "issued_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_code": {
+ "name": "project_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "c_gbn": {
+ "name": "c_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "d_gbn": {
+ "name": "d_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "degree_gbn": {
+ "name": "degree_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dept_gbn": {
+ "name": "dept_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "j_gbn": {
+ "name": "j_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s_gbn": {
+ "name": "s_gbn",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_stage_id": {
+ "name": "first_stage_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_stage_name": {
+ "name": "first_stage_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_stage_plan_date": {
+ "name": "first_stage_plan_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_stage_actual_date": {
+ "name": "first_stage_actual_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "second_stage_id": {
+ "name": "second_stage_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "second_stage_name": {
+ "name": "second_stage_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "second_stage_plan_date": {
+ "name": "second_stage_plan_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "second_stage_actual_date": {
+ "name": "second_stage_actual_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "all_stages": {
+ "name": "all_stages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_count": {
+ "name": "attachment_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "definition": "\n WITH \n -- 리비전별 첨부파일 집계\n revision_attachments AS (\n SELECT \n r.id as revision_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', da.id,\n 'revisionId', da.revision_id,\n 'fileName', da.file_name,\n 'filePath', da.file_path,\n 'fileSize', da.file_size,\n 'fileType', da.file_type,\n 'createdAt', da.created_at,\n 'updatedAt', da.updated_at\n ) ORDER BY da.created_at\n ) FILTER (WHERE da.id IS NOT NULL),\n '[]'::json\n ) as attachments\n FROM revisions r\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY r.id\n ),\n -- 스테이지별 리비전 집계 (첨부파일 포함)\n stage_revisions AS (\n SELECT \n ist.id as stage_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', r.id,\n 'issueStageId', r.issue_stage_id,\n 'revision', r.revision,\n 'uploaderType', r.uploader_type,\n 'uploaderId', r.uploader_id,\n 'uploaderName', r.uploader_name,\n 'comment', r.comment,\n 'usage', r.usage,\n 'usageType', r.usage_type,\n 'revisionStatus', r.revision_status,\n 'submittedDate', r.submitted_date,\n 'uploadedAt', r.uploaded_at,\n 'approvedDate', r.approved_date,\n 'reviewStartDate', r.review_start_date,\n 'rejectedDate', r.rejected_date,\n 'reviewerId', r.reviewer_id,\n 'reviewerName', r.reviewer_name,\n 'reviewComments', r.review_comments,\n 'createdAt', r.created_at,\n 'updatedAt', r.updated_at,\n 'attachments', ra.attachments\n ) ORDER BY r.created_at\n ) FILTER (WHERE r.id IS NOT NULL),\n '[]'::json\n ) as revisions\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN revision_attachments ra ON r.id = ra.revision_id\n GROUP BY ist.id\n ),\n -- 문서별 스테이지 집계 (리비전 포함)\n stage_aggregation AS (\n SELECT \n ist.document_id,\n json_agg(\n json_build_object(\n 'id', ist.id,\n 'stageName', ist.stage_name,\n 'stageStatus', ist.stage_status,\n 'stageOrder', ist.stage_order,\n 'planDate', ist.plan_date,\n 'actualDate', ist.actual_date,\n 'assigneeName', ist.assignee_name,\n 'priority', ist.priority,\n 'revisions', sr.revisions\n ) ORDER BY ist.stage_order\n ) as all_stages\n FROM issue_stages ist\n LEFT JOIN stage_revisions sr ON ist.id = sr.stage_id\n GROUP BY ist.document_id\n ),\n -- 첫 번째 스테이지 정보 (drawingKind에 따라 다른 조건)\n first_stage_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n ist.id as first_stage_id,\n ist.stage_name as first_stage_name,\n ist.plan_date as first_stage_plan_date,\n ist.actual_date as first_stage_actual_date\n FROM issue_stages ist\n JOIN documents d ON ist.document_id = d.id\n WHERE \n (d.drawing_kind = 'B4' AND LOWER(ist.stage_name) LIKE '%pre%') OR\n (d.drawing_kind = 'B3' AND LOWER(ist.stage_name) LIKE '%approval%') OR\n (d.drawing_kind = 'B5' AND LOWER(ist.stage_name) LIKE '%first%')\n ORDER BY ist.document_id, ist.stage_order ASC\n ),\n -- 두 번째 스테이지 정보 (drawingKind에 따라 다른 조건)\n second_stage_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n ist.id as second_stage_id,\n ist.stage_name as second_stage_name,\n ist.plan_date as second_stage_plan_date,\n ist.actual_date as second_stage_actual_date\n FROM issue_stages ist\n JOIN documents d ON ist.document_id = d.id\n WHERE \n (d.drawing_kind = 'B4' AND LOWER(ist.stage_name) LIKE '%work%') OR\n (d.drawing_kind = 'B3' AND LOWER(ist.stage_name) LIKE '%work%') OR\n (d.drawing_kind = 'B5' AND LOWER(ist.stage_name) LIKE '%second%')\n ORDER BY ist.document_id, ist.stage_order ASC\n ),\n -- 첨부파일 수 집계\n attachment_counts AS (\n SELECT \n ist.document_id,\n COUNT(da.id) as attachment_count\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY ist.document_id\n )\n \n SELECT \n d.id as document_id,\n d.doc_number,\n d.drawing_kind,\n d.vendor_doc_number,\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n \n -- B4 전용 필드들\n d.c_gbn,\n d.d_gbn,\n d.degree_gbn,\n d.dept_gbn,\n d.s_gbn,\n d.j_gbn,\n \n -- 프로젝트 및 벤더 정보\n p.code as project_code,\n v.vendor_name as vendor_name,\n v.vendor_code as vendor_code,\n \n -- 첫 번째 스테이지 정보\n fsi.first_stage_id,\n fsi.first_stage_name,\n fsi.first_stage_plan_date,\n fsi.first_stage_actual_date,\n \n -- 두 번째 스테이지 정보\n ssi.second_stage_id,\n ssi.second_stage_name,\n ssi.second_stage_plan_date,\n ssi.second_stage_actual_date,\n \n -- 전체 스테이지 (리비전 및 첨부파일 포함)\n COALESCE(sa.all_stages, '[]'::json) as all_stages,\n \n -- 기타\n COALESCE(ac.attachment_count, 0) as attachment_count,\n d.created_at,\n d.updated_at\n \n FROM documents d\n -- contracts, projects, vendors 테이블 JOIN\n LEFT JOIN contracts c ON d.contract_id = c.id\n INNER JOIN projects p ON c.project_id = p.id AND p.type = 'ship'\n LEFT JOIN vendors v ON c.vendor_id = v.id\n \n -- 스테이지 정보 JOIN\n LEFT JOIN first_stage_info fsi ON d.id = fsi.document_id\n LEFT JOIN second_stage_info ssi ON d.id = ssi.document_id\n LEFT JOIN stage_aggregation sa ON d.id = sa.document_id\n LEFT JOIN attachment_counts ac ON d.id = ac.document_id\n \n ORDER BY d.created_at DESC\n",
+ "name": "simplified_documents_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.sync_status_view": {
+ "columns": {
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_system": {
+ "name": "target_system",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_changes": {
+ "name": "total_changes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pending_changes": {
+ "name": "pending_changes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "synced_changes": {
+ "name": "synced_changes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "failed_changes": {
+ "name": "failed_changes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_sync_at": {
+ "name": "last_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_sync_at": {
+ "name": "next_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_enabled": {
+ "name": "sync_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n WITH change_stats AS (\n SELECT \n cl.contract_id,\n sc.target_system,\n COUNT(*) as total_changes,\n COUNT(CASE WHEN cl.is_synced = false AND cl.sync_attempts < sc.retry_max_attempts THEN 1 END) as pending_changes,\n COUNT(CASE WHEN cl.is_synced = true THEN 1 END) as synced_changes,\n COUNT(CASE WHEN cl.sync_attempts >= sc.retry_max_attempts AND cl.is_synced = false THEN 1 END) as failed_changes,\n MAX(cl.synced_at) as last_sync_at\n FROM change_logs cl\n CROSS JOIN sync_configs sc \n WHERE cl.contract_id = sc.contract_id\n AND (cl.target_systems IS NULL OR cl.target_systems @> to_jsonb(sc.target_system))\n GROUP BY cl.contract_id, sc.target_system\n )\n SELECT \n cs.contract_id,\n cs.target_system,\n COALESCE(cs.total_changes, 0) as total_changes,\n COALESCE(cs.pending_changes, 0) as pending_changes,\n COALESCE(cs.synced_changes, 0) as synced_changes,\n COALESCE(cs.failed_changes, 0) as failed_changes,\n cs.last_sync_at,\n CASE \n WHEN sc.sync_enabled = true AND sc.last_successful_sync IS NOT NULL \n THEN sc.last_successful_sync + (sc.sync_interval_minutes || ' minutes')::interval\n ELSE NULL\n END as next_sync_at,\n sc.sync_enabled\n FROM sync_configs sc\n LEFT JOIN change_stats cs ON sc.contract_id = cs.contract_id AND sc.target_system = cs.target_system\n",
+ "name": "sync_status_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_documents_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "doc_number": {
+ "name": "doc_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pic": {
+ "name": "pic",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issued_date": {
+ "name": "issued_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contract_id": {
+ "name": "contract_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "latest_stage_id": {
+ "name": "latest_stage_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_stage_name": {
+ "name": "latest_stage_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_stage_plan_date": {
+ "name": "latest_stage_plan_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_stage_actual_date": {
+ "name": "latest_stage_actual_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_id": {
+ "name": "latest_revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision": {
+ "name": "latest_revision",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_uploader_type": {
+ "name": "latest_revision_uploader_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_revision_uploader_name": {
+ "name": "latest_revision_uploader_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_count": {
+ "name": "attachment_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "definition": "\n SELECT \n d.id, \n d.doc_number,\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n \n (SELECT id FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_id,\n (SELECT stage_name FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_name,\n (SELECT plan_date FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_plan_date,\n (SELECT actual_date FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_actual_date,\n \n (SELECT r.id FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_id,\n (SELECT r.revision FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision,\n (SELECT r.uploader_type FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_uploader_type,\n (SELECT r.uploader_name FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_uploader_name,\n \n (SELECT COUNT(*) FROM document_attachments a JOIN revisions r ON a.revision_id = r.id JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id) AS attachment_count,\n \n d.created_at,\n d.updated_at\n FROM documents d\n JOIN contracts c ON d.contract_id = c.id\n ",
+ "name": "vendor_documents_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_candidates_with_vendor_info": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'COLLECTED'"
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "select \"vendor_candidates\".\"id\", \"vendor_candidates\".\"company_name\", \"vendor_candidates\".\"contact_email\", \"vendor_candidates\".\"contact_phone\", \"vendor_candidates\".\"tax_id\", \"vendor_candidates\".\"address\", \"vendor_candidates\".\"country\", \"vendor_candidates\".\"source\", \"vendor_candidates\".\"status\", \"vendor_candidates\".\"items\", \"vendor_candidates\".\"remark\", \"vendor_candidates\".\"created_at\", \"vendor_candidates\".\"updated_at\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", \"vendors\".\"created_at\" as \"vendor_created_at\", (\n SELECT l2.\"created_at\"\n FROM \"vendor_candidate_logs\" l2\n WHERE l2.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l2.\"action\" = 'status_change'\n ORDER BY l2.\"created_at\" DESC\n LIMIT 1\n ) as \"last_status_change_at\", (\n SELECT u.\"name\"\n FROM \"users\" u\n JOIN \"vendor_candidate_logs\" l3\n ON l3.\"user_id\" = u.\"id\"\n WHERE l3.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l3.\"action\" = 'status_change'\n ORDER BY l3.\"created_at\" DESC\n LIMIT 1\n ) as \"last_status_change_by\", (\n SELECT l4.\"created_at\"\n FROM \"vendor_candidate_logs\" l4\n WHERE l4.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l4.\"action\" = 'invite_sent'\n ORDER BY l4.\"created_at\" DESC\n LIMIT 1\n ) as \"last_invitation_at\", (\n SELECT u2.\"name\"\n FROM \"users\" u2\n JOIN \"vendor_candidate_logs\" l5\n ON l5.\"user_id\" = u2.\"id\"\n WHERE l5.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l5.\"action\" = 'invite_sent'\n ORDER BY l5.\"created_at\" DESC\n LIMIT 1\n ) as \"last_invitation_by\" from \"vendor_candidates\" left join \"vendors\" on \"vendor_candidates\".\"vendor_id\" = \"vendors\".\"id\"",
+ "name": "vendor_candidates_with_vendor_info",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_detail_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "business_size": {
+ "name": "business_size",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website": {
+ "name": "website",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING_REVIEW'"
+ },
+ "representative_name": {
+ "name": "representative_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_birth": {
+ "name": "representative_birth",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_email": {
+ "name": "representative_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_phone": {
+ "name": "representative_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "corporate_registration_number": {
+ "name": "corporate_registration_number",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credit_agency": {
+ "name": "credit_agency",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credit_rating": {
+ "name": "credit_rating",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cash_flow_rating": {
+ "name": "cash_flow_rating",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"id\", \"vendor_name\", \"vendor_code\", \"tax_id\", \"address\", \"business_size\", \"country\", \"phone\", \"email\", \"website\", \"status\", \"representative_name\", \"representative_birth\", \"representative_email\", \"representative_phone\", \"corporate_registration_number\", \"credit_agency\", \"credit_rating\", \"cash_flow_rating\", \"created_at\", \"updated_at\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', c.id,\n 'contactName', c.contact_name,\n 'contactPosition', c.contact_position,\n 'contactEmail', c.contact_email,\n 'contactPhone', c.contact_phone,\n 'isPrimary', c.is_primary\n )\n ),\n '[]'::json\n )\n FROM vendor_contacts c\n WHERE c.vendor_id = vendors.id)\n as \"contacts\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', a.id,\n 'fileName', a.file_name,\n 'filePath', a.file_path,\n 'attachmentType', a.attachment_type,\n 'createdAt', a.created_at\n )\n ORDER BY a.attachment_type, a.created_at DESC\n ),\n '[]'::json\n )\n FROM vendor_attachments a\n WHERE a.vendor_id = vendors.id)\n as \"attachments\", \n (SELECT COUNT(*)\n FROM vendor_attachments a\n WHERE a.vendor_id = vendors.id)\n as \"attachment_count\", \n (SELECT COUNT(*) \n FROM vendor_contacts c\n WHERE c.vendor_id = vendors.id)\n as \"contact_count\" from \"vendors\"",
+ "name": "vendor_detail_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_items_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"vendor_possible_items\".\"id\", \"vendor_possible_items\".\"vendor_id\", \"items\".\"item_name\", \"items\".\"item_code\", \"items\".\"description\", \"vendor_possible_items\".\"created_at\", \"vendor_possible_items\".\"updated_at\" from \"vendor_possible_items\" left join \"items\" on \"vendor_possible_items\".\"item_code\" = \"items\".\"item_code\"",
+ "name": "vendor_items_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_materials_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_of_measure": {
+ "name": "unit_of_measure",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "steel_type": {
+ "name": "steel_type",
+ "type": "varchar(2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grade_material": {
+ "name": "grade_material",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"vendor_possible_materials\".\"id\", \"vendor_possible_materials\".\"vendor_id\", \"materials\".\"item_name\", \"materials\".\"item_code\", \"materials\".\"description\", \"materials\".\"unit_of_measure\", \"materials\".\"steel_type\", \"materials\".\"grade_material\", \"vendor_possible_materials\".\"created_at\", \"vendor_possible_materials\".\"updated_at\" from \"vendor_possible_materials\" left join \"materials\" on \"vendor_possible_materials\".\"item_code\" = \"materials\".\"item_code\"",
+ "name": "vendor_materials_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendors_with_types": {
+ "columns": {},
+ "definition": "select \"vendors\".\"id\" as \"id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"tax_id\" as \"tax_id\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"phone\" as \"phone\", \"vendors\".\"email\" as \"email\", \"vendors\".\"business_size\" as \"business_size\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"status\", \"vendors\".\"vendor_type_id\" as \"vendor_type_id\", \"vendors\".\"representative_name\" as \"representative_name\", \"vendors\".\"representative_birth\" as \"representative_birth\", \"vendors\".\"representative_email\" as \"representative_email\", \"vendors\".\"representative_phone\" as \"representative_phone\", \"vendors\".\"corporate_registration_number\" as \"corporate_registration_number\", \"vendors\".\"items\" as \"items\", \"vendors\".\"credit_agency\" as \"credit_agency\", \"vendors\".\"credit_rating\" as \"credit_rating\", \"vendors\".\"cash_flow_rating\" as \"cash_flow_rating\", \"vendors\".\"created_at\" as \"created_at\", \"vendors\".\"updated_at\" as \"updated_at\", \"vendor_types\".\"name_ko\" as \"vendor_type_name\", \"vendor_types\".\"name_en\" as \"vendor_type_name_en\", \"vendor_types\".\"code\" as \"vendor_type_code\", \n CASE\n WHEN \"vendors\".\"status\" = 'ACTIVE' THEN '정규업체'\n WHEN \"vendors\".\"status\" IN ('INACTIVE', 'BLACKLISTED', 'REJECTED') THEN ''\n ELSE '잠재업체'\n END\n as \"vendor_category\" from \"vendors\" left join \"vendor_types\" on \"vendors\".\"vendor_type_id\" = \"vendor_types\".\"id\"",
+ "name": "vendors_with_types",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.basic_contract_view": {
+ "columns": {},
+ "definition": "select \"basic_contract\".\"id\" as \"id\", \"basic_contract\".\"template_id\" as \"template_id\", \"basic_contract\".\"vendor_id\" as \"vendor_id\", \"basic_contract\".\"requested_by\" as \"requested_by\", \"basic_contract\".\"status\" as \"basic_contract_status\", \"basic_contract\".\"created_at\" as \"created_at\", \"basic_contract\".\"updated_at\" as \"updated_at\", \"basic_contract\".\"updated_at\" as \"completed_at\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"email\" as \"vendor_email\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"users\".\"name\" as \"user_name\", \"basic_contract_templates\".\"template_name\" as \"template_name\", \"basic_contract_templates\".\"validity_period\" as \"validityPeriod\", \"basic_contract_templates\".\"file_path\" as \"file_path\", \"basic_contract_templates\".\"file_name\" as \"file_name\", \"basic_contract\".\"file_path\" as \"signed_file_path\" from \"basic_contract\" left join \"vendors\" on \"basic_contract\".\"vendor_id\" = \"vendors\".\"id\" left join \"users\" on \"basic_contract\".\"requested_by\" = \"users\".\"id\" left join \"basic_contract_templates\" on \"basic_contract\".\"template_id\" = \"basic_contract_templates\".\"id\"",
+ "name": "basic_contract_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.pr_items_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "procurement_rfqs_id": {
+ "name": "procurement_rfqs_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_item": {
+ "name": "rfq_item",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_item": {
+ "name": "pr_item",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_no": {
+ "name": "pr_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_code": {
+ "name": "material_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_category": {
+ "name": "material_category",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acc": {
+ "name": "acc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_description": {
+ "name": "material_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size": {
+ "name": "size",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "uom": {
+ "name": "uom",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gross_weight": {
+ "name": "gross_weight",
+ "type": "numeric(12, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 1
+ },
+ "gw_uom": {
+ "name": "gw_uom",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spec_no": {
+ "name": "spec_no",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spec_url": {
+ "name": "spec_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tracking_no": {
+ "name": "tracking_no",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "major_yn": {
+ "name": "major_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "project_def": {
+ "name": "project_def",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_sc": {
+ "name": "project_sc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_kl": {
+ "name": "project_kl",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_lc": {
+ "name": "project_lc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_dl": {
+ "name": "project_dl",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "select \"pr_items\".\"id\", \"pr_items\".\"procurement_rfqs_id\", \"pr_items\".\"rfq_item\", \"pr_items\".\"pr_item\", \"pr_items\".\"pr_no\", \"pr_items\".\"material_code\", \"pr_items\".\"material_category\", \"pr_items\".\"acc\", \"pr_items\".\"material_description\", \"pr_items\".\"size\", \"pr_items\".\"delivery_date\", \"pr_items\".\"quantity\", \"pr_items\".\"uom\", \"pr_items\".\"gross_weight\", \"pr_items\".\"gw_uom\", \"pr_items\".\"spec_no\", \"pr_items\".\"spec_url\", \"pr_items\".\"tracking_no\", \"pr_items\".\"major_yn\", \"pr_items\".\"project_def\", \"pr_items\".\"project_sc\", \"pr_items\".\"project_kl\", \"pr_items\".\"project_lc\", \"pr_items\".\"project_dl\", \"pr_items\".\"remark\", \"procurement_rfqs\".\"rfq_code\", \"procurement_rfqs\".\"item_code\", \"procurement_rfqs\".\"item_name\" from \"pr_items\" left join \"procurement_rfqs\" on \"pr_items\".\"procurement_rfqs_id\" = \"procurement_rfqs\".\"id\"",
+ "name": "pr_items_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.procurement_rfq_details_view": {
+ "columns": {},
+ "definition": "select \"rfq_details\".\"id\" as \"detail_id\", \"rfqs\".\"id\" as \"rfq_id\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"rfqs\".\"item_code\" as \"item_code\", \"rfqs\".\"item_name\" as \"item_name\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"country\" as \"vendor_country\", \"rfq_details\".\"currency\" as \"currency\", \"payment_terms\".\"code\" as \"payment_terms_code\", \"payment_terms\".\"description\" as \"payment_terms_description\", \"incoterms\".\"code\" as \"incoterms_code\", \"incoterms\".\"description\" as \"incoterms_description\", \"rfq_details\".\"incoterms_detail\" as \"incoterms_detail\", \"rfq_details\".\"delivery_date\" as \"delivery_date\", \"rfq_details\".\"tax_code\" as \"tax_code\", \"rfq_details\".\"place_of_shipping\" as \"place_of_shipping\", \"rfq_details\".\"place_of_destination\" as \"place_of_destination\", \"rfq_details\".\"material_price_related_yn\" as \"material_price_related_yn\", \"updated_by_user\".\"name\" as \"updated_by_user_name\", \"rfq_details\".\"updated_at\" as \"updated_at\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"rfqs\".\"id\"\n ) as \"pr_items_count\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"rfqs\".\"id\" \n AND major_yn = true\n ) as \"major_items_count\", (\n SELECT COUNT(*) \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"comment_count\", (\n SELECT created_at \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"last_comment_date\", (\n SELECT created_at \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\" AND is_vendor_comment = true\n ORDER BY created_at DESC LIMIT 1\n ) as \"last_vendor_comment_date\", (\n SELECT COUNT(*) \n FROM procurement_rfq_attachments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"attachment_count\", (\n SELECT COUNT(*) > 0\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"has_quotation\", (\n SELECT status\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"quotation_status\", (\n SELECT total_price\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"quotation_total_price\", (\n SELECT quotation_version\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY quotation_version DESC LIMIT 1\n ) as \"quotation_version\", (\n SELECT COUNT(DISTINCT quotation_version)\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"quotation_version_count\", (\n SELECT created_at\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY quotation_version DESC LIMIT 1\n ) as \"last_quotation_date\" from \"procurement_rfq_details\" \"rfq_details\" left join \"procurement_rfqs\" \"rfqs\" on \"rfq_details\".\"procurement_rfqs_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendors\" on \"rfq_details\".\"vendors_id\" = \"vendors\".\"id\" left join \"payment_terms\" on \"rfq_details\".\"payment_terms_code\" = \"payment_terms\".\"code\" left join \"incoterms\" on \"rfq_details\".\"incoterms_code\" = \"incoterms\".\"code\" left join \"users\" \"updated_by_user\" on \"rfq_details\".\"updated_by\" = \"updated_by_user\".\"id\"",
+ "name": "procurement_rfq_details_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.procurement_rfqs_view": {
+ "columns": {},
+ "definition": "select \"procurement_rfqs\".\"id\" as \"id\", \"procurement_rfqs\".\"rfq_code\" as \"rfq_code\", \"procurement_rfqs\".\"series\" as \"series\", \"procurement_rfqs\".\"rfq_sealed_yn\" as \"rfq_sealed_yn\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"procurement_rfqs\".\"item_code\" as \"item_code\", \"procurement_rfqs\".\"item_name\" as \"item_name\", \"procurement_rfqs\".\"status\" as \"status\", \"procurement_rfqs\".\"pic_code\" as \"pic_code\", \"procurement_rfqs\".\"rfq_send_date\" as \"rfq_send_date\", \"procurement_rfqs\".\"due_date\" as \"due_date\", (\n SELECT MIN(submitted_at)\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"procurement_rfqs\".\"id\"\n AND submitted_at IS NOT NULL\n ) as \"earliest_quotation_submitted_at\", \"created_by_user\".\"name\" as \"created_by_user_name\", \"sent_by_user\".\"name\" as \"sent_by_user_name\", \"procurement_rfqs\".\"updated_at\" as \"updated_at\", \"updated_by_user\".\"name\" as \"updated_by_user_name\", \"procurement_rfqs\".\"remark\" as \"remark\", (\n SELECT material_code \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n AND major_yn = true\n LIMIT 1\n ) as \"major_item_material_code\", (\n SELECT pr_no \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n AND major_yn = true\n LIMIT 1\n ) as \"po_no\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n ) as \"pr_items_count\" from \"procurement_rfqs\" left join \"projects\" on \"procurement_rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" \"created_by_user\" on \"procurement_rfqs\".\"created_by\" = \"created_by_user\".\"id\" left join \"users\" \"updated_by_user\" on \"procurement_rfqs\".\"updated_by\" = \"updated_by_user\".\"id\" left join \"users\" \"sent_by_user\" on \"procurement_rfqs\".\"sent_by\" = \"sent_by_user\".\"id\"",
+ "name": "procurement_rfqs_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.attachment_revision_history": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serial_no": {
+ "name": "serial_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_revision_id": {
+ "name": "client_revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_revision_no": {
+ "name": "client_revision_no",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_file_name": {
+ "name": "client_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_file_path": {
+ "name": "client_file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_file_size": {
+ "name": "client_file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_revision_comment": {
+ "name": "client_revision_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_revision_created_at": {
+ "name": "client_revision_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_latest_client_revision": {
+ "name": "is_latest_client_revision",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_vendor_responses": {
+ "name": "total_vendor_responses",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_vendors": {
+ "name": "responded_vendors",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pending_vendors": {
+ "name": "pending_vendors",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_response_files": {
+ "name": "total_response_files",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n ba.id as attachment_id,\n ba.attachment_type,\n ba.serial_no,\n \n -- 발주처 리비전 정보\n rev.id as client_revision_id,\n rev.revision_no as client_revision_no,\n rev.original_file_name as client_file_name,\n rev.file_size as client_file_size,\n rev.file_path as client_file_path,\n rev.revision_comment as client_revision_comment,\n rev.created_at as client_revision_created_at,\n rev.is_latest as is_latest_client_revision,\n \n -- 벤더 응답 통계\n COALESCE(response_stats.total_responses, 0) as total_vendor_responses,\n COALESCE(response_stats.responded_count, 0) as responded_vendors,\n COALESCE(response_stats.pending_count, 0) as pending_vendors,\n COALESCE(response_stats.total_files, 0) as total_response_files\n \n FROM b_rfqs br\n JOIN b_rfq_attachments ba ON br.id = ba.rfq_id\n JOIN b_rfq_attachment_revisions rev ON ba.id = rev.attachment_id\n LEFT JOIN (\n SELECT \n var.attachment_id,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count,\n COUNT(vra.id) as total_files\n FROM vendor_attachment_responses var\n LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id\n GROUP BY var.attachment_id\n ) response_stats ON ba.id = response_stats.attachment_id\n \n ORDER BY ba.id, rev.created_at DESC\n",
+ "name": "attachment_revision_history",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.attachments_with_latest_revision": {
+ "columns": {
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serial_no": {
+ "name": "serial_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_revision": {
+ "name": "current_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_id": {
+ "name": "revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_comment": {
+ "name": "revision_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_name": {
+ "name": "created_by_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n a.id as attachment_id,\n a.attachment_type,\n a.serial_no,\n a.rfq_id,\n a.description,\n a.current_revision,\n \n r.id as revision_id,\n r.file_name,\n r.original_file_name,\n r.file_path,\n r.file_size,\n r.file_type,\n r.revision_comment,\n \n a.created_by,\n u.name as created_by_name,\n a.created_at,\n a.updated_at\n FROM b_rfq_attachments a\n LEFT JOIN b_rfq_attachment_revisions r ON a.latest_revision_id = r.id\n LEFT JOIN users u ON a.created_by = u.id\n ",
+ "name": "attachments_with_latest_revision",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.b_rfqs_master": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pic_code": {
+ "name": "pic_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pic_name": {
+ "name": "pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "eng_pic_name": {
+ "name": "eng_pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_no": {
+ "name": "package_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_name": {
+ "name": "package_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_code": {
+ "name": "project_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_name": {
+ "name": "project_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_type": {
+ "name": "project_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_company": {
+ "name": "project_company",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_flag": {
+ "name": "project_flag",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_site": {
+ "name": "project_site",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_attachments": {
+ "name": "total_attachments",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.description,\n br.status,\n br.due_date,\n br.pic_code,\n br.pic_name,\n br.eng_pic_name,\n br.package_no,\n br.package_name,\n br.project_id,\n p.code as project_code,\n p.name as project_name,\n p.type as project_type,\n br.project_company,\n br.project_flag,\n br.project_site,\n COALESCE(att_count.total_attachments, 0) as total_attachments,\n br.created_at,\n br.updated_at\n FROM b_rfqs br\n LEFT JOIN projects p ON br.project_id = p.id\n LEFT JOIN (\n SELECT rfq_id, COUNT(*) as total_attachments\n FROM b_rfq_attachments\n GROUP BY rfq_id\n ) att_count ON br.id = att_count.rfq_id\n",
+ "name": "b_rfqs_master",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.final_rfq_detail": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_status": {
+ "name": "rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_rfq_id": {
+ "name": "final_rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_rfq_status": {
+ "name": "final_rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_country": {
+ "name": "vendor_country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_business_size": {
+ "name": "vendor_business_size",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "valid_date": {
+ "name": "valid_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_date": {
+ "name": "delivery_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_description": {
+ "name": "incoterms_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_terms_code": {
+ "name": "payment_terms_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_terms_description": {
+ "name": "payment_terms_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_code": {
+ "name": "tax_code",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "place_of_shipping": {
+ "name": "place_of_shipping",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "place_of_destination": {
+ "name": "place_of_destination",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "short_list": {
+ "name": "short_list",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "return_yn": {
+ "name": "return_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cp_request_yn": {
+ "name": "cp_request_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prject_gtc_yn": {
+ "name": "prject_gtc_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "firsttime_yn": {
+ "name": "firsttime_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "material_price_related_yn": {
+ "name": "material_price_related_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "return_revision": {
+ "name": "return_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc": {
+ "name": "gtc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc_valid_date": {
+ "name": "gtc_valid_date",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "classification": {
+ "name": "classification",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sparepart": {
+ "name": "sparepart",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_remark": {
+ "name": "vendor_remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n fr.id as final_rfq_id,\n fr.final_rfq_status,\n fr.vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n fr.due_date,\n fr.valid_date,\n fr.delivery_date,\n fr.incoterms_code,\n inc.description as incoterms_description,\n fr.payment_terms_code,\n pt.description as payment_terms_description,\n fr.currency,\n fr.tax_code,\n fr.place_of_shipping,\n fr.place_of_destination,\n fr.short_list,\n fr.return_yn,\n fr.cp_request_yn,\n fr.prject_gtc_yn,\n fr.firsttime_yn,\n fr.material_price_related_yn,\n fr.return_revision,\n fr.gtc,\n fr.gtc_valid_date,\n fr.classification,\n fr.sparepart,\n fr.remark,\n fr.vendor_remark,\n fr.created_at,\n fr.updated_at\n FROM b_rfqs br\n JOIN final_rfq fr ON br.id = fr.rfq_id\n LEFT JOIN vendors v ON fr.vendor_id = v.id\n LEFT JOIN incoterms inc ON fr.incoterms_code = inc.code\n LEFT JOIN payment_terms pt ON fr.payment_terms_code = pt.code\n",
+ "name": "final_rfq_detail",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.initial_rfq_detail": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_status": {
+ "name": "rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_rfq_id": {
+ "name": "initial_rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_rfq_status": {
+ "name": "initial_rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_category": {
+ "name": "vendor_category",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_country": {
+ "name": "vendor_country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_business_size": {
+ "name": "vendor_business_size",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "valid_date": {
+ "name": "valid_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_code": {
+ "name": "incoterms_code",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "incoterms_description": {
+ "name": "incoterms_description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "short_list": {
+ "name": "short_list",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "return_yn": {
+ "name": "return_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cp_request_yn": {
+ "name": "cp_request_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prject_gtc_yn": {
+ "name": "prject_gtc_yn",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "return_revision": {
+ "name": "return_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_revision": {
+ "name": "rfq_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc": {
+ "name": "gtc",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gtc_valid_date": {
+ "name": "gtc_valid_date",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "classification": {
+ "name": "classification",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sparepart": {
+ "name": "sparepart",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n ir.id as initial_rfq_id,\n ir.initial_rfq_status,\n ir.vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n v.vendor_category as vendor_category,\n ir.due_date,\n ir.valid_date,\n ir.incoterms_code,\n inc.description as incoterms_description,\n ir.short_list,\n ir.return_yn,\n ir.cp_request_yn,\n ir.prject_gtc_yn,\n ir.return_revision,\n ir.rfq_revision,\n ir.gtc,\n ir.gtc_valid_date,\n ir.classification,\n ir.sparepart,\n ir.created_at,\n ir.updated_at\n FROM b_rfqs br\n JOIN initial_rfq ir ON br.id = ir.rfq_id\n LEFT JOIN vendors_with_types v ON ir.vendor_id = v.id\n LEFT JOIN incoterms inc ON ir.incoterms_code = inc.code\n",
+ "name": "initial_rfq_detail",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.rfq_dashboard": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_code": {
+ "name": "project_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_name": {
+ "name": "project_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_no": {
+ "name": "package_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "package_name": {
+ "name": "package_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pic_code": {
+ "name": "pic_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pic_name": {
+ "name": "pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "eng_pic_name": {
+ "name": "eng_pic_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_company": {
+ "name": "project_company",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_flag": {
+ "name": "project_flag",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_site": {
+ "name": "project_site",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_attachments": {
+ "name": "total_attachments",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_vendor_count": {
+ "name": "initial_vendor_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_vendor_count": {
+ "name": "final_vendor_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_response_rate": {
+ "name": "initial_response_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_response_rate": {
+ "name": "final_response_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "overall_progress": {
+ "name": "overall_progress",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "days_to_deadline": {
+ "name": "days_to_deadline",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by_name": {
+ "name": "updated_by_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by_email": {
+ "name": "updated_by_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n -- ② SELECT 절 확장 -------------------------------------------\n SELECT\n br.id AS rfq_id,\n br.rfq_code,\n br.description,\n br.status,\n br.due_date,\n p.code AS project_code,\n p.name AS project_name,\n br.package_no,\n br.package_name,\n br.pic_code,\n br.pic_name,\n br.eng_pic_name,\n br.project_company,\n br.project_flag,\n br.project_site,\n br.remark,\n \n -- 첨부/벤더 요약 -----------------------\n COALESCE(att_count.total_attachments, 0) AS total_attachments,\n COALESCE(init_summary.vendor_count, 0) AS initial_vendor_count,\n COALESCE(final_summary.vendor_count, 0) AS final_vendor_count,\n COALESCE(init_summary.avg_response_rate, 0) AS initial_response_rate,\n COALESCE(final_summary.avg_response_rate, 0) AS final_response_rate,\n \n -- 진행률·마감까지 일수 --------------\n CASE \n WHEN br.status = 'DRAFT' THEN 0\n WHEN br.status = 'Doc. Received' THEN 10\n WHEN br.status = 'PIC Assigned' THEN 20\n WHEN br.status = 'Doc. Confirmed' THEN 30\n WHEN br.status = 'Init. RFQ Sent' THEN 40\n WHEN br.status = 'Init. RFQ Answered' THEN 50\n WHEN br.status = 'TBE started' THEN 60\n WHEN br.status = 'TBE finished' THEN 70\n WHEN br.status = 'Final RFQ Sent' THEN 80\n WHEN br.status = 'Quotation Received' THEN 90\n WHEN br.status = 'Vendor Selected' THEN 100\n ELSE 0\n END AS overall_progress,\n (br.due_date - CURRENT_DATE) AS days_to_deadline,\n \n br.created_at,\n br.updated_at,\n \n -- 💡 추가되는 컬럼 -------------------\n upd.name AS updated_by_name,\n upd.email AS updated_by_email\n FROM b_rfqs br\n LEFT JOIN projects p ON br.project_id = p.id\n \n -- ③ 사용자 정보 조인 --------------------\n LEFT JOIN users upd ON br.updated_by = upd.id\n \n -- (나머지 이미 있던 JOIN 들은 그대로) -----\n LEFT JOIN (\n SELECT rfq_id, COUNT(*) AS total_attachments\n FROM b_rfq_attachments\n GROUP BY rfq_id\n ) att_count ON br.id = att_count.rfq_id\n \n LEFT JOIN (\n SELECT \n rfq_id, \n COUNT(DISTINCT vendor_id) AS vendor_count,\n AVG(response_rate) AS avg_response_rate\n FROM vendor_response_summary\n WHERE rfq_type = 'INITIAL'\n GROUP BY rfq_id\n ) init_summary ON br.id = init_summary.rfq_id\n \n LEFT JOIN (\n SELECT \n rfq_id, \n COUNT(DISTINCT vendor_id) AS vendor_count,\n AVG(response_rate) AS avg_response_rate\n FROM vendor_response_summary\n WHERE rfq_type = 'FINAL'\n GROUP BY rfq_id\n ) final_summary ON br.id = final_summary.rfq_id\n ",
+ "name": "rfq_dashboard",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.rfq_progress_summary": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_status": {
+ "name": "rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "days_to_deadline": {
+ "name": "days_to_deadline",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_attachments": {
+ "name": "total_attachments",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachments_with_multiple_revisions": {
+ "name": "attachments_with_multiple_revisions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_client_revisions": {
+ "name": "total_client_revisions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_vendor_count": {
+ "name": "initial_vendor_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_total_responses": {
+ "name": "initial_total_responses",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_responded_count": {
+ "name": "initial_responded_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_up_to_date_count": {
+ "name": "initial_up_to_date_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_version_mismatch_count": {
+ "name": "initial_version_mismatch_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_response_rate": {
+ "name": "initial_response_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_version_match_rate": {
+ "name": "initial_version_match_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_vendor_count": {
+ "name": "final_vendor_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_total_responses": {
+ "name": "final_total_responses",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_responded_count": {
+ "name": "final_responded_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_up_to_date_count": {
+ "name": "final_up_to_date_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_version_mismatch_count": {
+ "name": "final_version_mismatch_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_response_rate": {
+ "name": "final_response_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "final_version_match_rate": {
+ "name": "final_version_match_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_response_files": {
+ "name": "total_response_files",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n br.due_date,\n (br.due_date - CURRENT_DATE) as days_to_deadline,\n \n -- 첨부파일 통계\n attachment_stats.total_attachments,\n attachment_stats.attachments_with_multiple_revisions,\n attachment_stats.total_client_revisions,\n \n -- Initial RFQ 통계\n COALESCE(initial_stats.vendor_count, 0) as initial_vendor_count,\n COALESCE(initial_stats.total_responses, 0) as initial_total_responses,\n COALESCE(initial_stats.responded_count, 0) as initial_responded_count,\n COALESCE(initial_stats.up_to_date_count, 0) as initial_up_to_date_count,\n COALESCE(initial_stats.version_mismatch_count, 0) as initial_version_mismatch_count,\n COALESCE(initial_stats.response_rate, 0) as initial_response_rate,\n COALESCE(initial_stats.version_match_rate, 0) as initial_version_match_rate,\n \n -- Final RFQ 통계\n COALESCE(final_stats.vendor_count, 0) as final_vendor_count,\n COALESCE(final_stats.total_responses, 0) as final_total_responses,\n COALESCE(final_stats.responded_count, 0) as final_responded_count,\n COALESCE(final_stats.up_to_date_count, 0) as final_up_to_date_count,\n COALESCE(final_stats.version_mismatch_count, 0) as final_version_mismatch_count,\n COALESCE(final_stats.response_rate, 0) as final_response_rate,\n COALESCE(final_stats.version_match_rate, 0) as final_version_match_rate,\n \n COALESCE(file_stats.total_files, 0) as total_response_files\n \n FROM b_rfqs br\n LEFT JOIN (\n SELECT \n ba.rfq_id,\n COUNT(*) as total_attachments,\n COUNT(CASE WHEN rev_count.total_revisions > 1 THEN 1 END) as attachments_with_multiple_revisions,\n SUM(rev_count.total_revisions) as total_client_revisions\n FROM b_rfq_attachments ba\n LEFT JOIN (\n SELECT \n attachment_id,\n COUNT(*) as total_revisions\n FROM b_rfq_attachment_revisions\n GROUP BY attachment_id\n ) rev_count ON ba.id = rev_count.attachment_id\n GROUP BY ba.rfq_id\n ) attachment_stats ON br.id = attachment_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(DISTINCT var.vendor_id) as vendor_count,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count,\n COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count,\n ROUND(\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(*), 0), 2\n ) as response_rate,\n ROUND(\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2\n ) as version_match_rate\n FROM b_rfqs br\n JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id\n JOIN vendor_attachment_responses var ON vrd.response_id = var.id\n WHERE var.rfq_type = 'INITIAL'\n GROUP BY br.id\n ) initial_stats ON br.id = initial_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(DISTINCT var.vendor_id) as vendor_count,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count,\n COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count,\n ROUND(\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(*), 0), 2\n ) as response_rate,\n ROUND(\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2\n ) as version_match_rate\n FROM b_rfqs br\n JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id\n JOIN vendor_attachment_responses var ON vrd.response_id = var.id\n WHERE var.rfq_type = 'FINAL'\n GROUP BY br.id\n ) final_stats ON br.id = final_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(vra.id) as total_files\n FROM b_rfqs br\n JOIN b_rfq_attachments ba ON br.id = ba.rfq_id\n JOIN vendor_attachment_responses var ON ba.id = var.attachment_id\n LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id\n GROUP BY br.id\n ) file_stats ON br.id = file_stats.rfq_id\n",
+ "name": "rfq_progress_summary",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_response_attachments_enhanced": {
+ "columns": {
+ "response_attachment_id": {
+ "name": "response_attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_response_id": {
+ "name": "vendor_response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_record_id": {
+ "name": "rfq_record_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_revision": {
+ "name": "current_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_revision": {
+ "name": "responded_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_comment": {
+ "name": "response_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_comment": {
+ "name": "vendor_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_request_comment": {
+ "name": "revision_request_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_at": {
+ "name": "responded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_requested_at": {
+ "name": "revision_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serial_no": {
+ "name": "serial_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_country": {
+ "name": "vendor_country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_revision_id": {
+ "name": "latest_client_revision_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_revision_no": {
+ "name": "latest_client_revision_no",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_file_name": {
+ "name": "latest_client_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_version_matched": {
+ "name": "is_version_matched",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_lag": {
+ "name": "version_lag",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "needs_update": {
+ "name": "needs_update",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_sequence": {
+ "name": "file_sequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_latest_response_file": {
+ "name": "is_latest_response_file",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n vra.id as response_attachment_id,\n vra.vendor_response_id,\n vra.file_name,\n vra.original_file_name,\n vra.file_path,\n vra.file_size,\n vra.file_type,\n vra.description,\n vra.uploaded_at,\n \n -- 응답 기본 정보\n var.attachment_id,\n var.vendor_id,\n var.rfq_type,\n var.rfq_record_id,\n var.response_status,\n var.current_revision,\n var.responded_revision,\n \n -- 코멘트 (새로 추가된 필드 포함)\n var.response_comment,\n var.vendor_comment,\n var.revision_request_comment,\n \n -- 날짜 (새로 추가된 필드 포함)\n var.requested_at,\n var.responded_at,\n var.revision_requested_at,\n \n -- 첨부파일 정보\n ba.attachment_type,\n ba.serial_no,\n ba.rfq_id,\n \n -- 벤더 정보\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n \n -- 발주처 현재 리비전 정보\n latest_rev.id as latest_client_revision_id,\n latest_rev.revision_no as latest_client_revision_no,\n latest_rev.original_file_name as latest_client_file_name,\n \n -- 리비전 비교\n CASE \n WHEN var.responded_revision = ba.current_revision THEN true \n ELSE false \n END as is_version_matched,\n \n -- 버전 차이 계산 (Rev.0, Rev.1 형태 가정)\n CASE \n WHEN var.responded_revision IS NULL THEN NULL\n WHEN ba.current_revision IS NULL THEN NULL\n ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - \n CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER)\n END as version_lag,\n \n CASE \n WHEN var.response_status = 'RESPONDED' \n AND var.responded_revision != ba.current_revision THEN true \n ELSE false \n END as needs_update,\n \n -- 파일 순서\n ROW_NUMBER() OVER (\n PARTITION BY var.id \n ORDER BY vra.uploaded_at DESC\n ) as file_sequence,\n \n -- 최신 응답 파일 여부\n CASE \n WHEN ROW_NUMBER() OVER (\n PARTITION BY var.id \n ORDER BY vra.uploaded_at DESC\n ) = 1 THEN true \n ELSE false \n END as is_latest_response_file\n \n FROM vendor_response_attachments_b vra\n JOIN vendor_attachment_responses var ON vra.vendor_response_id = var.id\n JOIN b_rfq_attachments ba ON var.attachment_id = ba.id\n LEFT JOIN vendors v ON var.vendor_id = v.id\n LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id\n",
+ "name": "vendor_response_attachments_enhanced",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_response_detail": {
+ "columns": {
+ "response_id": {
+ "name": "response_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_record_id": {
+ "name": "rfq_record_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_type": {
+ "name": "attachment_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serial_no": {
+ "name": "serial_no",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_description": {
+ "name": "attachment_description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_country": {
+ "name": "vendor_country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_revision": {
+ "name": "current_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_revision": {
+ "name": "responded_revision",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_comment": {
+ "name": "response_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_comment": {
+ "name": "vendor_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_request_comment": {
+ "name": "revision_request_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_at": {
+ "name": "responded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_requested_at": {
+ "name": "revision_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_revision_no": {
+ "name": "latest_client_revision_no",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_file_name": {
+ "name": "latest_client_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_file_size": {
+ "name": "latest_client_file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_client_revision_comment": {
+ "name": "latest_client_revision_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_version_matched": {
+ "name": "is_version_matched",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_lag": {
+ "name": "version_lag",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "needs_update": {
+ "name": "needs_update",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_multiple_revisions": {
+ "name": "has_multiple_revisions",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_response_files": {
+ "name": "total_response_files",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_response_file_name": {
+ "name": "latest_response_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_response_file_size": {
+ "name": "latest_response_file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_response_uploaded_at": {
+ "name": "latest_response_uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "effective_status": {
+ "name": "effective_status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n var.id as response_id,\n ba.rfq_id,\n br.rfq_code,\n var.rfq_type,\n var.rfq_record_id,\n \n -- 첨부파일 정보\n ba.id as attachment_id,\n ba.attachment_type,\n ba.serial_no,\n ba.description as attachment_description,\n \n -- 벤더 정보\n v.id as vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n \n -- 응답 상태\n var.response_status,\n var.current_revision,\n var.responded_revision,\n \n -- 코멘트 (새로 추가된 필드 포함)\n var.response_comment,\n var.vendor_comment,\n var.revision_request_comment,\n \n -- 날짜 (새로 추가된 필드 포함)\n var.requested_at,\n var.responded_at,\n var.revision_requested_at,\n \n -- 발주처 최신 리비전\n latest_rev.revision_no as latest_client_revision_no,\n latest_rev.original_file_name as latest_client_file_name,\n latest_rev.file_size as latest_client_file_size,\n latest_rev.revision_comment as latest_client_revision_comment,\n \n -- 리비전 분석\n CASE \n WHEN var.responded_revision = ba.current_revision THEN true \n ELSE false \n END as is_version_matched,\n \n CASE \n WHEN var.responded_revision IS NULL OR ba.current_revision IS NULL THEN NULL\n ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - \n CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER)\n END as version_lag,\n \n CASE \n WHEN var.response_status = 'RESPONDED' \n AND var.responded_revision != ba.current_revision THEN true \n ELSE false \n END as needs_update,\n \n CASE \n WHEN revision_count.total_revisions > 1 THEN true \n ELSE false \n END as has_multiple_revisions,\n \n -- 응답 파일 정보\n COALESCE(file_stats.total_files, 0) as total_response_files,\n file_stats.latest_file_name as latest_response_file_name,\n file_stats.latest_file_size as latest_response_file_size,\n file_stats.latest_uploaded_at as latest_response_uploaded_at,\n \n -- 효과적인 상태\n CASE \n WHEN var.response_status = 'NOT_RESPONDED' THEN 'NOT_RESPONDED'\n WHEN var.response_status = 'WAIVED' THEN 'WAIVED'\n WHEN var.response_status = 'REVISION_REQUESTED' THEN 'REVISION_REQUESTED'\n WHEN var.response_status = 'RESPONDED' AND var.responded_revision = ba.current_revision THEN 'UP_TO_DATE'\n WHEN var.response_status = 'RESPONDED' AND var.responded_revision != ba.current_revision THEN 'VERSION_MISMATCH'\n ELSE var.response_status\n END as effective_status\n \n FROM vendor_attachment_responses var\n JOIN b_rfq_attachments ba ON var.attachment_id = ba.id\n JOIN b_rfqs br ON ba.rfq_id = br.id\n LEFT JOIN vendors v ON var.vendor_id = v.id\n LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id\n LEFT JOIN (\n SELECT \n attachment_id,\n COUNT(*) as total_revisions\n FROM b_rfq_attachment_revisions\n GROUP BY attachment_id\n ) revision_count ON ba.id = revision_count.attachment_id\n LEFT JOIN (\n SELECT \n vendor_response_id,\n COUNT(*) as total_files,\n MAX(original_file_name) as latest_file_name,\n MAX(file_size) as latest_file_size,\n MAX(uploaded_at) as latest_uploaded_at\n FROM vendor_response_attachments_b\n GROUP BY vendor_response_id\n ) file_stats ON var.id = file_stats.vendor_response_id\n",
+ "name": "vendor_response_detail",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.vendor_response_summary": {
+ "columns": {
+ "rfq_id": {
+ "name": "rfq_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_code": {
+ "name": "rfq_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_status": {
+ "name": "rfq_status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_country": {
+ "name": "vendor_country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor_business_size": {
+ "name": "vendor_business_size",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rfq_type": {
+ "name": "rfq_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_attachments": {
+ "name": "total_attachments",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responded_count": {
+ "name": "responded_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pending_count": {
+ "name": "pending_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "waived_count": {
+ "name": "waived_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision_requested_count": {
+ "name": "revision_requested_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_rate": {
+ "name": "response_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completion_rate": {
+ "name": "completion_rate",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n v.id as vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n var.rfq_type,\n COUNT(var.id) as total_attachments,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count,\n COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END) as waived_count,\n COUNT(CASE WHEN var.response_status = 'REVISION_REQUESTED' THEN 1 END) as revision_requested_count,\n ROUND(\n (COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status != 'WAIVED' THEN 1 END), 0)), \n 2\n ) as response_rate,\n ROUND(\n ((COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) + \n COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END)) * 100.0 / COUNT(var.id)), \n 2\n ) as completion_rate\n FROM b_rfqs br\n JOIN b_rfq_attachments bra ON br.id = bra.rfq_id\n JOIN vendor_attachment_responses var ON bra.id = var.attachment_id\n JOIN vendors v ON var.vendor_id = v.id\n GROUP BY br.id, br.rfq_code, br.status, v.id, v.vendor_code, v.vendor_name, v.country, v.business_size, var.rfq_type\n",
+ "name": "vendor_response_summary",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.tech_vendor_candidates_with_vendor_info": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'COLLECTED'"
+ },
+ "items": {
+ "name": "items",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "definition": "select \"tech_vendor_candidates\".\"id\", \"tech_vendor_candidates\".\"company_name\", \"tech_vendor_candidates\".\"contact_email\", \"tech_vendor_candidates\".\"contact_phone\", \"tech_vendor_candidates\".\"tax_id\", \"tech_vendor_candidates\".\"address\", \"tech_vendor_candidates\".\"country\", \"tech_vendor_candidates\".\"source\", \"tech_vendor_candidates\".\"status\", \"tech_vendor_candidates\".\"items\", \"tech_vendor_candidates\".\"remark\", \"tech_vendor_candidates\".\"created_at\", \"tech_vendor_candidates\".\"updated_at\", \"tech_vendors\".\"vendor_name\", \"tech_vendors\".\"vendor_code\", \"tech_vendors\".\"created_at\" as \"vendor_created_at\" from \"tech_vendor_candidates\" left join \"tech_vendors\" on \"tech_vendor_candidates\".\"vendor_id\" = \"tech_vendors\".\"id\"",
+ "name": "tech_vendor_candidates_with_vendor_info",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.tech_vendor_detail_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tax_id": {
+ "name": "tax_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address": {
+ "name": "address",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country_eng": {
+ "name": "country_eng",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country_fab": {
+ "name": "country_fab",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_name": {
+ "name": "agent_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_phone": {
+ "name": "agent_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_email": {
+ "name": "agent_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website": {
+ "name": "website",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "tech_vendor_type": {
+ "name": "tech_vendor_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "representative_name": {
+ "name": "representative_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_email": {
+ "name": "representative_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_phone": {
+ "name": "representative_phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "representative_birth": {
+ "name": "representative_birth",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"id\", \"vendor_name\", \"vendor_code\", \"tax_id\", \"address\", \"country\", \"country_eng\", \"country_fab\", \"agent_name\", \"agent_phone\", \"agent_email\", \"phone\", \"email\", \"website\", \"status\", \"tech_vendor_type\", \"representative_name\", \"representative_email\", \"representative_phone\", \"representative_birth\", \"created_at\", \"updated_at\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', c.id,\n 'contactName', c.contact_name,\n 'contactPosition', c.contact_position,\n 'contactEmail', c.contact_email,\n 'contactPhone', c.contact_phone,\n 'isPrimary', c.is_primary\n )\n ),\n '[]'::json\n )\n FROM vendor_contacts c\n WHERE c.vendor_id = tech_vendors.id)\n as \"contacts\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', a.id,\n 'fileName', a.file_name,\n 'filePath', a.file_path,\n 'attachmentType', a.attachment_type,\n 'createdAt', a.created_at\n )\n ORDER BY a.attachment_type, a.created_at DESC\n ),\n '[]'::json\n )\n FROM tech_vendor_attachments a\n WHERE a.vendor_id = tech_vendors.id)\n as \"attachments\", \n (SELECT COUNT(*)\n FROM tech_vendor_attachments a\n WHERE a.vendor_id = tech_vendors.id)\n as \"attachment_count\", \n (SELECT COUNT(*) \n FROM vendor_contacts c\n WHERE c.vendor_id = tech_vendors.id)\n as \"contact_count\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'itemCode', i.item_code,\n 'itemName', it.item_name\n )\n ),\n '[]'::json\n )\n FROM tech_vendor_possible_items i\n LEFT JOIN items it ON i.item_code = it.item_code\n WHERE i.vendor_id = tech_vendors.id)\n as \"possible_items\", \n (SELECT COUNT(*) \n FROM tech_vendor_possible_items i\n WHERE i.vendor_id = tech_vendors.id)\n as \"item_count\" from \"tech_vendors\"",
+ "name": "tech_vendor_detail_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.tech_vendor_items_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "vendor_id": {
+ "name": "vendor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_code": {
+ "name": "item_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_name": {
+ "name": "item_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"tech_vendor_possible_items\".\"id\", \"tech_vendor_possible_items\".\"vendor_id\", \"items\".\"item_code\", \"items\".\"item_name\", \"tech_vendor_possible_items\".\"created_at\", \"tech_vendor_possible_items\".\"updated_at\" from \"tech_vendor_possible_items\" left join \"items\" on \"tech_vendor_possible_items\".\"item_code\" = \"items\".\"item_code\"",
+ "name": "tech_vendor_items_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.esg_evaluations_view": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serial_number": {
+ "name": "serial_number",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inspection_item": {
+ "name": "inspection_item",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"esg_evaluations\".\"id\", \"esg_evaluations\".\"serial_number\", \"esg_evaluations\".\"category\", \"esg_evaluations\".\"inspection_item\", \"esg_evaluations\".\"is_active\", \"esg_evaluations\".\"created_at\", \"esg_evaluations\".\"updated_at\", count(distinct \"esg_evaluation_items\".\"id\") as \"total_evaluation_items\", count(\"esg_answer_options\".\"id\") as \"total_answer_options\", coalesce(sum(\"esg_answer_options\".\"score\"), 0) as \"max_possible_score\", \n (\n SELECT array_agg(evaluation_item order by order_index) \n FROM esg_evaluation_items \n WHERE esg_evaluation_id = \"esg_evaluations\".\"id\" \n AND is_active = true \n AND evaluation_item is not null\n )\n as \"evaluation_items_list\" from \"esg_evaluations\" left join \"esg_evaluation_items\" on \"esg_evaluations\".\"id\" = \"esg_evaluation_items\".\"esg_evaluation_id\" AND \"esg_evaluation_items\".\"is_active\" = true left join \"esg_answer_options\" on \"esg_evaluation_items\".\"id\" = \"esg_answer_options\".\"esg_evaluation_item_id\" AND \"esg_answer_options\".\"is_active\" = true group by \"esg_evaluations\".\"id\", \"esg_evaluations\".\"serial_number\", \"esg_evaluations\".\"category\", \"esg_evaluations\".\"inspection_item\", \"esg_evaluations\".\"is_active\", \"esg_evaluations\".\"created_at\", \"esg_evaluations\".\"updated_at\"",
+ "name": "esg_evaluations_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.evaluation_targets_with_departments": {
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "evaluation_year": {
+ "name": "evaluation_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "division": {
+ "name": "division",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_code": {
+ "name": "vendor_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor_name": {
+ "name": "vendor_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domestic_foreign": {
+ "name": "domestic_foreign",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "material_type": {
+ "name": "material_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "consensus_status": {
+ "name": "consensus_status",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "admin_comment": {
+ "name": "admin_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consolidated_comment": {
+ "name": "consolidated_comment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_at": {
+ "name": "confirmed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmed_by": {
+ "name": "confirmed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ld_claim_count": {
+ "name": "ld_claim_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "ld_claim_amount": {
+ "name": "ld_claim_amount",
+ "type": "numeric(15, 2)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "ld_claim_currency": {
+ "name": "ld_claim_currency",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'KRW'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "definition": "select \"evaluation_targets\".\"id\", \"evaluation_targets\".\"evaluation_year\", \"evaluation_targets\".\"division\", \"evaluation_targets\".\"vendor_code\", \"evaluation_targets\".\"vendor_name\", \"evaluation_targets\".\"domestic_foreign\", \"evaluation_targets\".\"material_type\", \"evaluation_targets\".\"status\", \"evaluation_targets\".\"consensus_status\", \"evaluation_targets\".\"admin_comment\", \"evaluation_targets\".\"consolidated_comment\", \"evaluation_targets\".\"confirmed_at\", \"evaluation_targets\".\"confirmed_by\", \"evaluation_targets\".\"ld_claim_count\", \"evaluation_targets\".\"ld_claim_amount\", \"evaluation_targets\".\"ld_claim_currency\", \"evaluation_targets\".\"created_at\", \"evaluation_targets\".\"updated_at\", order_reviewer.name as \"order_reviewer_name\", order_reviewer.email as \"order_reviewer_email\", order_etr.department_name_from as \"order_department_name\", order_review.is_approved as \"order_is_approved\", order_review.reviewed_at as \"order_reviewed_at\", procurement_reviewer.name as \"procurement_reviewer_name\", procurement_reviewer.email as \"procurement_reviewer_email\", procurement_etr.department_name_from as \"procurement_department_name\", procurement_review.is_approved as \"procurement_is_approved\", procurement_review.reviewed_at as \"procurement_reviewed_at\", quality_reviewer.name as \"quality_reviewer_name\", quality_reviewer.email as \"quality_reviewer_email\", quality_etr.department_name_from as \"quality_department_name\", quality_review.is_approved as \"quality_is_approved\", quality_review.reviewed_at as \"quality_reviewed_at\", design_reviewer.name as \"design_reviewer_name\", design_reviewer.email as \"design_reviewer_email\", design_etr.department_name_from as \"design_department_name\", design_review.is_approved as \"design_is_approved\", design_review.reviewed_at as \"design_reviewed_at\", cs_reviewer.name as \"cs_reviewer_name\", cs_reviewer.email as \"cs_reviewer_email\", cs_etr.department_name_from as \"cs_department_name\", cs_review.is_approved as \"cs_is_approved\", cs_review.reviewed_at as \"cs_reviewed_at\" from \"evaluation_targets\" left join evaluation_target_reviewers order_etr on \"evaluation_targets\".\"id\" = order_etr.evaluation_target_id AND order_etr.department_code = 'ORDER_EVAL' left join users order_reviewer on order_etr.reviewer_user_id = order_reviewer.id left join evaluation_target_reviews order_review on \"evaluation_targets\".\"id\" = order_review.evaluation_target_id AND order_review.reviewer_user_id = order_reviewer.id left join evaluation_target_reviewers procurement_etr on \"evaluation_targets\".\"id\" = procurement_etr.evaluation_target_id AND procurement_etr.department_code = 'PROCUREMENT_EVAL' left join users procurement_reviewer on procurement_etr.reviewer_user_id = procurement_reviewer.id left join evaluation_target_reviews procurement_review on \"evaluation_targets\".\"id\" = procurement_review.evaluation_target_id AND procurement_review.reviewer_user_id = procurement_reviewer.id left join evaluation_target_reviewers quality_etr on \"evaluation_targets\".\"id\" = quality_etr.evaluation_target_id AND quality_etr.department_code = 'QUALITY_EVAL' left join users quality_reviewer on quality_etr.reviewer_user_id = quality_reviewer.id left join evaluation_target_reviews quality_review on \"evaluation_targets\".\"id\" = quality_review.evaluation_target_id AND quality_review.reviewer_user_id = quality_reviewer.id left join evaluation_target_reviewers design_etr on \"evaluation_targets\".\"id\" = design_etr.evaluation_target_id AND design_etr.department_code = 'DESIGN_EVAL' left join users design_reviewer on design_etr.reviewer_user_id = design_reviewer.id left join evaluation_target_reviews design_review on \"evaluation_targets\".\"id\" = design_review.evaluation_target_id AND design_review.reviewer_user_id = design_reviewer.id left join evaluation_target_reviewers cs_etr on \"evaluation_targets\".\"id\" = cs_etr.evaluation_target_id AND cs_etr.department_code = 'CS_EVAL' left join users cs_reviewer on cs_etr.reviewer_user_id = cs_reviewer.id left join evaluation_target_reviews cs_review on \"evaluation_targets\".\"id\" = cs_review.evaluation_target_id AND cs_review.reviewer_user_id = cs_reviewer.id",
+ "name": "evaluation_targets_with_departments",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ },
+ "public.project_gtc_view": {
+ "columns": {},
+ "definition": "select \"projects\".\"id\" as \"id\", \"projects\".\"code\" as \"code\", \"projects\".\"name\" as \"name\", \"projects\".\"type\" as \"type\", \"projects\".\"created_at\" as \"project_created_at\", \"projects\".\"updated_at\" as \"project_updated_at\", \"project_gtc_files\".\"id\" as \"gtc_file_id\", \"project_gtc_files\".\"file_name\" as \"fileName\", \"project_gtc_files\".\"file_path\" as \"filePath\", \"project_gtc_files\".\"original_file_name\" as \"originalFileName\", \"project_gtc_files\".\"file_size\" as \"fileSize\", \"project_gtc_files\".\"mime_type\" as \"mimeType\", \"project_gtc_files\".\"created_at\" as \"gtcCreatedAt\", \"project_gtc_files\".\"updated_at\" as \"gtcUpdatedAt\" from \"projects\" left join \"project_gtc_files\" on \"projects\".\"id\" = \"project_gtc_files\".\"project_id\"",
+ "name": "project_gtc_view",
+ "schema": "public",
+ "isExisting": false,
+ "materialized": false
+ }
+ },
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+} \ No newline at end of file
diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json
index 70b29664..8d2737ff 100644
--- a/db/migrations/meta/_journal.json
+++ b/db/migrations/meta/_journal.json
@@ -1100,6 +1100,13 @@
"when": 1750310865435,
"tag": "0156_premium_tyger_tiger",
"breakpoints": true
+ },
+ {
+ "idx": 157,
+ "version": "7",
+ "when": 1750379942192,
+ "tag": "0157_oval_sunfire",
+ "breakpoints": true
}
]
} \ No newline at end of file
diff --git a/db/schema/evaluationCriteria.ts b/db/schema/evaluationCriteria.ts
new file mode 100644
index 00000000..23a987cf
--- /dev/null
+++ b/db/schema/evaluationCriteria.ts
@@ -0,0 +1,137 @@
+/* IMPORT */
+import {
+ decimal,
+ integer,
+ pgTable,
+ pgView,
+ serial,
+ text,
+ timestamp,
+ varchar,
+} from 'drizzle-orm/pg-core';
+import { eq, relations } from 'drizzle-orm';
+
+// ----------------------------------------------------------------------------------------------------
+
+/* CONSTANTS */
+const REG_EVAL_CRITERIA_CATEGORY = [
+ { label: 'CS', value: 'customer-service' },
+ { label: '관리자', value: 'administrator' },
+ { label: '구매', value: 'procurement' },
+ { label: '설계', value: 'design' },
+ { label: '조달', value: 'sourcing' },
+ { label: '품질', value: 'quality' },
+];
+const REG_EVAL_CRITERIA_ITEM = [
+ { label: '가점항목', value: 'customer-service' },
+ { label: '납기', value: 'delivery' },
+ { label: '경영현황', value: 'management-status' },
+ { label: '감점항목', value: 'penalty-item' },
+ { label: '구매', value: 'procurement' },
+ { label: '품질', value: 'quality' },
+];
+const REG_EVAL_CRITERIA_CATEGORY2 = [
+ { label: '공정', value: 'processScore' },
+ { label: '가격', value: 'priceScore' },
+ { label: '납기', value: 'deliveryScore' },
+ { label: '자율평가', value: 'selfEvaluationScore' },
+];
+
+const REG_EVAL_CRITERIA_CATEGORY_ENUM = REG_EVAL_CRITERIA_CATEGORY.map(c => c.value) as [string, ...string[]];
+const REG_EVAL_CRITERIA_CATEGORY_ENUM2 = REG_EVAL_CRITERIA_CATEGORY2.map(c => c.value) as [string, ...string[]];
+const REG_EVAL_CRITERIA_ITEM_ENUM = REG_EVAL_CRITERIA_ITEM.map(c => c.value) as [string, ...string[]];
+
+// ----------------------------------------------------------------------------------------------------
+
+/* 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('category', { enum: REG_EVAL_CRITERIA_CATEGORY_ENUM2, 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(),
+});
+const regEvalCriteriaDetails = pgTable('reg_eval_criteria_details', {
+ id: serial('id').primaryKey(),
+ criteriaId: integer('criteria_id')
+ .notNull()
+ .references(() => regEvalCriteria.id, { onDelete: 'cascade' }),
+ detail: text('detail').notNull(),
+ orderIndex: integer('order_index').default(0).notNull(),
+ scoreEquipShip: decimal('score_equip_ship', { precision: 5, scale: 2 }),
+ scoreEquipMarine: decimal('score_equip_marine', { precision: 5, scale: 2 }),
+ scoreBulkShip: decimal('score_bulk_ship', { precision: 5, scale: 2 }),
+ scoreBulkMarine: decimal('score_bulk_marine', { precision: 5, scale: 2 }),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// ----------------------------------------------------------------------------------------------------
+
+/* VIEWS */
+const regEvalCriteriaView = pgView('reg_eval_criteria_view').as((qb) =>
+ qb
+ .select({
+ id: regEvalCriteria.id,
+ category: regEvalCriteria.category,
+ item: regEvalCriteria.item,
+ classification: regEvalCriteria.classification,
+ range: regEvalCriteria.range,
+ detailId: regEvalCriteriaDetails.id,
+ detail: regEvalCriteriaDetails.detail,
+ orderIndex: regEvalCriteriaDetails.orderIndex,
+ scoreEquipShip: regEvalCriteriaDetails.scoreEquipShip,
+ scoreEquipMarine: regEvalCriteriaDetails.scoreEquipMarine,
+ scoreBulkShip: regEvalCriteriaDetails.scoreBulkShip,
+ scoreBulkMarine: regEvalCriteriaDetails.scoreBulkMarine,
+ })
+ .from(regEvalCriteria)
+ .leftJoin(regEvalCriteriaDetails, eq(regEvalCriteria.id, regEvalCriteriaDetails.criteriaId))
+ .orderBy(regEvalCriteria.id, regEvalCriteriaDetails.orderIndex)
+);
+
+// ----------------------------------------------------------------------------------------------------
+
+/* RELATIONS */
+const regEvalCriteriaRelations = relations(regEvalCriteria, ({ many }) => ({
+ details: many(regEvalCriteriaDetails),
+}));
+const regEvalCriteriaDetailsRelations = relations(regEvalCriteriaDetails, ({one}) => ({
+ criteria: one(regEvalCriteria, {
+ fields: [regEvalCriteriaDetails.criteriaId],
+ references: [regEvalCriteria.id],
+ }),
+}));
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TYPES */
+type RegEvalCriteria = typeof regEvalCriteria.$inferSelect;
+type NewRegEvalCriteria = typeof regEvalCriteria.$inferInsert;
+type RegEvalCriteriaDetails = typeof regEvalCriteriaDetails.$inferSelect;
+type NewRegEvalCriteriaDetails = typeof regEvalCriteriaDetails.$inferInsert;
+type RegEvalCriteriaWithDetails = RegEvalCriteria & { criteriaDetails: RegEvalCriteriaDetails[] };
+type RegEvalCriteriaView = typeof regEvalCriteriaView.$inferSelect;
+
+// ----------------------------------------------------------------------------------------------------
+
+/* Export */
+export {
+ REG_EVAL_CRITERIA_CATEGORY,
+ REG_EVAL_CRITERIA_ITEM,
+ regEvalCriteria,
+ regEvalCriteriaDetails,
+ regEvalCriteriaDetailsRelations,
+ regEvalCriteriaRelations,
+ regEvalCriteriaView,
+ type NewRegEvalCriteria,
+ type NewRegEvalCriteriaDetails,
+ type RegEvalCriteriaView,
+ type RegEvalCriteriaWithDetails,
+ type RegEvalCriteria,
+ type RegEvalCriteriaDetails,
+}; \ No newline at end of file
diff --git a/db/schema/evaluationTarget.ts b/db/schema/evaluationTarget.ts
index 4eff1c19..915641c8 100644
--- a/db/schema/evaluationTarget.ts
+++ b/db/schema/evaluationTarget.ts
@@ -3,6 +3,7 @@ import { eq , sql, relations} from "drizzle-orm";
import { vendors } from "./vendors";
import { users } from "./users";
import { contracts } from "./contract";
+import { regEvalCriteriaDetails } from "./evaluationCriteria";
// 평가 대상 메인 테이블
export const evaluationTargets = pgTable("evaluation_targets", {
@@ -178,7 +179,7 @@ export const orderRecordsRelations = relations(contracts, ({ one }) => ({
}),
}));
-// 평가 담당 부서 코드 상수 (조직 API와 매핑)
+// 평가 담당 부서 코드 상수
export const EVALUATION_DEPARTMENT_CODES = {
ORDER_EVAL: "ORDER_EVAL", // 발주 평가 담당
PROCUREMENT_EVAL: "PROCUREMENT_EVAL", // 조달 평가 담당
@@ -192,7 +193,7 @@ export type EvaluationDepartmentCode = keyof typeof EVALUATION_DEPARTMENT_CODES;
// ============= TypeScript 타입 정의 =============
export type EvaluationTargetStatus = "PENDING" | "CONFIRMED" | "EXCLUDED";
- export type Division = "OCEAN" | "SHIPYARD";
+ export type Division = "PLANT" | "SHIP";
export type MaterialType = "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK";
export type DomesticForeign = "DOMESTIC" | "FOREIGN";
@@ -383,3 +384,339 @@ export const evaluationTargetsWithDepartments = pgView("evaluation_targets_with_
// 타입 정의
export type EvaluationTargetWithDepartments = typeof evaluationTargetsWithDepartments.$inferSelect;
+
+
+export const periodicEvaluations = pgTable("periodic_evaluations", {
+ id: serial("id").primaryKey(),
+
+ // 평가 대상 참조
+ evaluationTargetId: integer("evaluation_target_id")
+ .references(() => evaluationTargets.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 평가 기본 정보
+ evaluationPeriod: varchar("evaluation_period", { length: 20 }).notNull(), // "상반기", "하반기", "연간" 등
+
+ // 업체 제출 관련
+ documentsSubmitted: boolean("documents_submitted").default(false),
+ submissionDate: timestamp("submission_date"),
+ submissionDeadline: timestamp("submission_deadline"),
+
+ // 평가 점수 (최종 확정)
+ finalScore: decimal("final_score", { precision: 5, scale: 2 }),
+ finalGrade: varchar("final_grade", {
+ length: 5,
+ enum: ["S", "A", "B", "C", "D"]
+ }),
+
+ // 평가 점수 (평가자 평균)
+ evaluationScore: decimal("evaluation_score", { precision: 5, scale: 2 }),
+ evaluationGrade: varchar("evaluation_grade", {
+ length: 5,
+ enum: ["S", "A", "B", "C", "D"]
+ }),
+
+ // 평가항목별 점수
+ processScore: decimal("process_score", { precision: 5, scale: 2 }).default("0"), // 공정
+ priceScore: decimal("price_score", { precision: 5, scale: 2 }).default("0"), // 가격
+ deliveryScore: decimal("delivery_score", { precision: 5, scale: 2 }).default("0"), // 납기
+ selfEvaluationScore: decimal("self_evaluation_score", { precision: 5, scale: 2 }).default("0"), // 자율평가
+
+ // 합계 점수
+ totalScore: decimal("total_score", { precision: 5, scale: 2 }).default("0"),
+
+ // 가점/감점
+ participationBonus: decimal("participation_bonus", { precision: 5, scale: 2 }).default("0"), // 참여도 가점
+ qualityDeduction: decimal("quality_deduction", { precision: 5, scale: 2 }).default("0"), // 품질 감점
+
+ // 평가 상태
+ status: varchar("status", {
+ length: 30,
+ enum: ["PENDING_SUBMISSION", "SUBMITTED", "IN_REVIEW", "REVIEW_COMPLETED", "FINALIZED"]
+ }).notNull().default("PENDING_SUBMISSION"),
+
+ // 평가 완료 정보
+ reviewCompletedAt: timestamp("review_completed_at"),
+ finalizedAt: timestamp("finalized_at"),
+ finalizedBy: integer("finalized_by").references(() => users.id),
+
+ // 비고
+ evaluationNote: text("evaluation_note"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+}, (table) => ({
+ // 같은 평가대상에 같은 기간에는 하나의 평가만
+ uniqueEvaluationTarget: unique("unique_evaluation_target")
+ .on(table.evaluationTargetId, table.evaluationPeriod),
+}));
+
+
+// 2. 리뷰어별 개별 평가 테이블
+export const reviewerEvaluations = pgTable("reviewer_evaluations", {
+ id: serial("id").primaryKey(),
+
+ // 정기평가 참조
+ periodicEvaluationId: integer("periodic_evaluation_id")
+ .references(() => periodicEvaluations.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 리뷰어 정보 (evaluationTargetReviewers 참조)
+ evaluationTargetReviewerId: integer("evaluation_target_reviewer_id")
+ .references(() => evaluationTargetReviewers.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 평가항목별 점수 (카테고리별 합산 점수)
+ processScore: decimal("process_score", { precision: 5, scale: 2 }),
+ priceScore: decimal("price_score", { precision: 5, scale: 2 }),
+ deliveryScore: decimal("delivery_score", { precision: 5, scale: 2 }),
+ selfEvaluationScore: decimal("self_evaluation_score", { precision: 5, scale: 2 }),
+
+ // 가점/감점
+ participationBonus: decimal("participation_bonus", { precision: 5, scale: 2 }).default("0"),
+ qualityDeduction: decimal("quality_deduction", { precision: 5, scale: 2 }).default("0"),
+
+ // 리뷰어 총점
+ totalScore: decimal("total_score", { precision: 5, scale: 2 }),
+ grade: varchar("grade", {
+ length: 5,
+ enum: ["S", "A", "B", "C", "D"]
+ }),
+
+ // 평가 완료 여부
+ isCompleted: boolean("is_completed").default(false),
+ completedAt: timestamp("completed_at"),
+
+ // 리뷰어 의견
+ reviewerComment: text("reviewer_comment"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+}, (table) => ({
+ // 같은 평가에 같은 리뷰어는 하나의 평가만
+ uniqueReviewerEvaluation: unique("unique_reviewer_evaluation")
+ .on(table.periodicEvaluationId, table.evaluationTargetReviewerId),
+}));
+
+// 2-1. 리뷰어별 세부 평가 점수 테이블 (평가표의 각 항목별 점수)
+export const reviewerEvaluationDetails = pgTable("reviewer_evaluation_details", {
+ id: serial("id").primaryKey(),
+
+ // 리뷰어 평가 참조
+ reviewerEvaluationId: integer("reviewer_evaluation_id")
+ .references(() => reviewerEvaluations.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 평가 기준 참조
+ regEvalCriteriaDetailsId: integer("reg_eval_criteria_details_id")
+ .references(() => regEvalCriteriaDetails.id)
+ .notNull(),
+
+ // 리뷰어가 매긴 점수
+ score: decimal("score", { precision: 5, scale: 2 }).notNull(),
+
+ // 세부 의견
+ comment: text("comment"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+}, (table) => ({
+ // 같은 리뷰어 평가에서 같은 평가 기준은 하나의 점수만
+ uniqueReviewerCriteria: unique("unique_reviewer_criteria")
+ .on(table.reviewerEvaluationId, table.regEvalCriteriaDetailsId),
+}));
+
+// 1. periodicEvaluations relations
+export const periodicEvaluationsRelations = relations(periodicEvaluations, ({ one, many }) => ({
+ // 평가 대상
+ evaluationTarget: one(evaluationTargets, {
+ fields: [periodicEvaluations.evaluationTargetId],
+ references: [evaluationTargets.id],
+ }),
+
+ // 최종 확정자
+ finalizedByUser: one(users, {
+ fields: [periodicEvaluations.finalizedBy],
+ references: [users.id],
+ }),
+
+ // 리뷰어별 평가들
+ reviewerEvaluations: many(reviewerEvaluations),
+
+}));
+
+// 2. reviewerEvaluations relations
+export const reviewerEvaluationsRelations = relations(reviewerEvaluations, ({ one, many }) => ({
+ // 정기평가
+ periodicEvaluation: one(periodicEvaluations, {
+ fields: [reviewerEvaluations.periodicEvaluationId],
+ references: [periodicEvaluations.id],
+ }),
+
+ // 평가 대상 리뷰어
+ evaluationTargetReviewer: one(evaluationTargetReviewers, {
+ fields: [reviewerEvaluations.evaluationTargetReviewerId],
+ references: [evaluationTargetReviewers.id],
+ }),
+
+ // 세부 평가 점수들
+ evaluationDetails: many(reviewerEvaluationDetails),
+}));
+
+// 3. reviewerEvaluationDetails relations
+export const reviewerEvaluationDetailsRelations = relations(reviewerEvaluationDetails, ({ one }) => ({
+ // 리뷰어 평가
+ reviewerEvaluation: one(reviewerEvaluations, {
+ fields: [reviewerEvaluationDetails.reviewerEvaluationId],
+ references: [reviewerEvaluations.id],
+ }),
+
+ // 평가 기준 세부사항
+ regEvalCriteriaDetail: one(regEvalCriteriaDetails, {
+ fields: [reviewerEvaluationDetails.regEvalCriteriaDetailsId],
+ references: [regEvalCriteriaDetails.id],
+ }),
+}));
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TYPES */
+type PeriodicEvaluation = typeof periodicEvaluations.$inferSelect;
+type NewPeriodicEvaluation = typeof periodicEvaluations.$inferInsert;
+
+type ReviewerEvaluation = typeof reviewerEvaluations.$inferSelect;
+type NewReviewerEvaluation = typeof reviewerEvaluations.$inferInsert;
+
+type ReviewerEvaluationDetail = typeof reviewerEvaluationDetails.$inferSelect;
+type NewReviewerEvaluationDetail = typeof reviewerEvaluationDetails.$inferInsert;
+
+// 관계 포함 타입들
+type PeriodicEvaluationWithRelations = PeriodicEvaluation & {
+ evaluationTarget?: typeof evaluationTargets.$inferSelect;
+ finalizedByUser?: typeof users.$inferSelect;
+ reviewerEvaluations?: ReviewerEvaluationWithRelations[];
+};
+
+type ReviewerEvaluationWithRelations = ReviewerEvaluation & {
+ periodicEvaluation?: PeriodicEvaluation;
+ evaluationTargetReviewer?: typeof evaluationTargetReviewers.$inferSelect;
+ evaluationDetails?: ReviewerEvaluationDetailWithRelations[];
+};
+
+type ReviewerEvaluationDetailWithRelations = ReviewerEvaluationDetail & {
+ reviewerEvaluation?: ReviewerEvaluation;
+ regEvalCriteriaDetail?: typeof regEvalCriteriaDetails.$inferSelect;
+};
+
+export const periodicEvaluationsView = pgView('periodic_evaluations_view').as((qb) =>
+ qb
+ .select({
+ // ═══════════════════════════════════════════════════════════════
+ // 정기평가 기본 정보 (평가 대상 핵심 정보 포함)
+ // ═══════════════════════════════════════════════════════════════
+ id: periodicEvaluations.id,
+ evaluationTargetId: periodicEvaluations.evaluationTargetId,
+
+ // 평가 대상 핵심 정보 (조인으로 가져와서 기본 정보로 포함)
+ evaluationYear: evaluationTargets.evaluationYear,
+ division: evaluationTargets.division,
+ vendorId: evaluationTargets.vendorId,
+ vendorCode: evaluationTargets.vendorCode,
+ vendorName: evaluationTargets.vendorName,
+ domesticForeign: evaluationTargets.domesticForeign,
+ materialType: evaluationTargets.materialType,
+
+ // 평가 기간
+ evaluationPeriod: periodicEvaluations.evaluationPeriod,
+
+ // 업체 제출 관련
+ documentsSubmitted: periodicEvaluations.documentsSubmitted,
+ submissionDate: periodicEvaluations.submissionDate,
+ submissionDeadline: periodicEvaluations.submissionDeadline,
+
+ // 평가 점수 (최종 확정)
+ finalScore: periodicEvaluations.finalScore,
+ finalGrade: periodicEvaluations.finalGrade,
+
+ // 평가 점수 (평가자 평균)
+ evaluationScore: periodicEvaluations.evaluationScore,
+ evaluationGrade: periodicEvaluations.evaluationGrade,
+
+ // 평가항목별 점수
+ processScore: periodicEvaluations.processScore,
+ priceScore: periodicEvaluations.priceScore,
+ deliveryScore: periodicEvaluations.deliveryScore,
+ selfEvaluationScore: periodicEvaluations.selfEvaluationScore,
+
+ // 합계 점수
+ totalScore: periodicEvaluations.totalScore,
+
+ // 가점/감점
+ participationBonus: periodicEvaluations.participationBonus,
+ qualityDeduction: periodicEvaluations.qualityDeduction,
+
+ // 평가 상태
+ status: periodicEvaluations.status,
+
+ // 평가 완료 정보
+ reviewCompletedAt: periodicEvaluations.reviewCompletedAt,
+ finalizedAt: periodicEvaluations.finalizedAt,
+ finalizedBy: periodicEvaluations.finalizedBy,
+
+ // 비고
+ evaluationNote: periodicEvaluations.evaluationNote,
+
+ // 생성/수정일
+ createdAt: periodicEvaluations.createdAt,
+ updatedAt: periodicEvaluations.updatedAt,
+
+ // ═══════════════════════════════════════════════════════════════
+ // 평가 대상 추가 정보 (evaluationTargets 조인)
+ // ═══════════════════════════════════════════════════════════════
+ evaluationTargetStatus: evaluationTargets.status,
+ evaluationTargetAdminComment: evaluationTargets.adminComment,
+ evaluationTargetConsolidatedComment: evaluationTargets.consolidatedComment,
+ evaluationTargetConsensusStatus: evaluationTargets.consensusStatus,
+ evaluationTargetConfirmedAt: evaluationTargets.confirmedAt,
+
+ // ═══════════════════════════════════════════════════════════════
+ // 리뷰어 통계 (서브쿼리로 계산)
+ // ═══════════════════════════════════════════════════════════════
+ totalReviewers: sql<number>`(
+ SELECT COUNT(*)::int
+ FROM ${reviewerEvaluations} re
+ WHERE re.periodic_evaluation_id = ${periodicEvaluations.id}
+ )`.as('total_reviewers'),
+
+ completedReviewers: sql<number>`(
+ SELECT COUNT(*)::int
+ FROM ${reviewerEvaluations} re
+ WHERE re.periodic_evaluation_id = ${periodicEvaluations.id}
+ AND re.is_completed = true
+ )`.as('completed_reviewers'),
+
+ pendingReviewers: sql<number>`(
+ SELECT COUNT(*)::int
+ FROM ${reviewerEvaluations} re
+ WHERE re.periodic_evaluation_id = ${periodicEvaluations.id}
+ AND re.is_completed = false
+ )`.as('pending_reviewers'),
+
+ // ═══════════════════════════════════════════════════════════════
+ // 최종 확정자 정보
+ // ═══════════════════════════════════════════════════════════════
+ finalizedByUserName: users.name,
+ finalizedByUserEmail: users.email,
+ })
+ .from(periodicEvaluations)
+ .leftJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id))
+ .leftJoin(users, eq(periodicEvaluations.finalizedBy, users.id))
+ .orderBy(periodicEvaluations.createdAt)
+);
+
+
+// ================================================================
+// TYPES
+// ================================================================
+
+export type PeriodicEvaluationView = typeof periodicEvaluationsView.$inferSelect;
diff --git a/db/schema/index.ts b/db/schema/index.ts
index fc59692d..480207f9 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -20,6 +20,7 @@ export * from './bRfq';
export * from './techVendors';
export * from './evaluation';
export * from './evaluationTarget';
+export * from './projectGtc';
// MDG SOAP 수신용
// export * from './MDG/modelMaster'
diff --git a/db/schema/projectGtc.ts b/db/schema/projectGtc.ts
new file mode 100644
index 00000000..220e42df
--- /dev/null
+++ b/db/schema/projectGtc.ts
@@ -0,0 +1,45 @@
+import { pgTable, pgView, timestamp, integer, varchar, serial } from 'drizzle-orm/pg-core';
+import { projects } from './projects';
+import { eq, sql } from "drizzle-orm";
+
+export const projectGtcFiles = pgTable('project_gtc_files', {
+ id: serial("id").primaryKey(),
+ projectId: integer('project_id').references(() => projects.id).notNull(),
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 1024 }).notNull(),
+ originalFileName: varchar("original_file_name", { length: 255 }).notNull(),
+ fileSize: integer('file_size'),
+ mimeType: varchar("mime_type", { length: 100 }),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// Project GTC 뷰 (프로젝트 정보와 파일 정보를 조인)
+export const projectGtcView = pgView('project_gtc_view').as((qb) => {
+ return qb
+ .select({
+ // 프로젝트 정보
+ id: sql<number>`${projects.id}`.as('id'),
+ code: sql<string>`${projects.code}`.as('code'),
+ name: sql<string>`${projects.name}`.as('name'),
+ type: sql<string>`${projects.type}`.as('type'),
+ projectCreatedAt: sql<Date>`${projects.createdAt}`.as('project_created_at'),
+ projectUpdatedAt: sql<Date>`${projects.updatedAt}`.as('project_updated_at'),
+
+ // GTC 파일 정보
+ gtcFileId: sql<number | null>`${projectGtcFiles.id}`.as('gtc_file_id'),
+ fileName: sql<string | null>`${projectGtcFiles.fileName}`.as('fileName'),
+ filePath: sql<string | null>`${projectGtcFiles.filePath}`.as('filePath'),
+ originalFileName: sql<string | null>`${projectGtcFiles.originalFileName}`.as('originalFileName'),
+ fileSize: sql<number | null>`${projectGtcFiles.fileSize}`.as('fileSize'),
+ mimeType: sql<string | null>`${projectGtcFiles.mimeType}`.as('mimeType'),
+ gtcCreatedAt: sql<Date | null>`${projectGtcFiles.createdAt}`.as('gtcCreatedAt'),
+ gtcUpdatedAt: sql<Date | null>`${projectGtcFiles.updatedAt}`.as('gtcUpdatedAt'),
+ })
+ .from(projects)
+ .leftJoin(projectGtcFiles, eq(projects.id, projectGtcFiles.projectId));
+});
+
+// 타입 정의
+export type ProjectGtcFile = typeof projectGtcFiles.$inferSelect;
+export type ProjectGtcView = typeof projectGtcView.$inferSelect; \ No newline at end of file
diff --git a/db/schema/techSales.ts b/db/schema/techSales.ts
index 334bf6bb..744d22cc 100644
--- a/db/schema/techSales.ts
+++ b/db/schema/techSales.ts
@@ -34,6 +34,8 @@ import {
integer,
numeric,
date,
+ json,
+ index,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { biddingProjects } from "./projects";
@@ -60,6 +62,7 @@ export const TECH_SALES_QUOTATION_STATUSES = {
SUBMITTED: "Submitted",
REVISED: "Revised",
ACCEPTED: "Accepted",
+ REJECTED: "Rejected",
} as const;
export type TechSalesQuotationStatus = typeof TECH_SALES_QUOTATION_STATUSES[keyof typeof TECH_SALES_QUOTATION_STATUSES];
@@ -90,6 +93,12 @@ export const TECH_SALES_QUOTATION_STATUS_CONFIG = {
description: "승인된 견적서",
color: "text-green-600",
},
+ [TECH_SALES_QUOTATION_STATUSES.REJECTED]: {
+ label: "거절됨",
+ variant: "destructive" as const,
+ description: "거절된 견적서",
+ color: "text-red-600",
+ },
} as const;
// ===== 스키마 정의 =====
@@ -241,6 +250,37 @@ export const techSalesVendorQuotations = pgTable(
}
);
+// 기술영업 벤더 견적서 revision 히스토리 테이블 (이전 버전 스냅샷 저장)
+export const techSalesVendorQuotationRevisions = pgTable(
+ "tech_sales_vendor_quotation_revisions",
+ {
+ id: serial("id").primaryKey(),
+ quotationId: integer("quotation_id")
+ .notNull()
+ .references(() => techSalesVendorQuotations.id, { onDelete: "cascade" }),
+
+ // 버전 정보
+ version: integer("version").notNull(),
+
+ // 이전 데이터 JSON 스냅샷
+ snapshot: json("snapshot").notNull(),
+
+ // 변경 사유
+ changeReason: text("change_reason"),
+ revisionNote: text("revision_note"),
+
+ // 변경자 정보
+ revisedBy: integer("revised_by"),
+ revisedAt: timestamp("revised_at").defaultNow().notNull(),
+ },
+ (table) => ({
+ quotationVersionIdx: index("tech_sales_quotation_revisions_quotation_version_idx").on(
+ table.quotationId,
+ table.version
+ ),
+ })
+);
+
export const techSalesRfqComments = pgTable(
"tech_sales_rfq_comments",
{
@@ -299,6 +339,28 @@ export const techSalesRfqCommentAttachments = pgTable("tech_sales_rfq_comment_at
uploadedAt: timestamp("uploaded_at").defaultNow().notNull(),
});
+// 기술영업 벤더 견적서 첨부파일 테이블
+export const techSalesVendorQuotationAttachments = pgTable("tech_sales_vendor_quotation_attachments", {
+ id: serial("id").primaryKey(),
+ quotationId: integer("quotation_id")
+ .notNull()
+ .references(() => techSalesVendorQuotations.id, { onDelete: "cascade" }),
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ originalFileName: varchar("original_file_name", { length: 255 }).notNull(),
+ fileSize: integer("file_size").notNull(),
+ fileType: varchar("file_type", { length: 100 }),
+ filePath: varchar("file_path", { length: 500 }).notNull(),
+ description: text("description"), // 파일 설명
+ uploadedBy: integer("uploaded_by").references(() => users.id, {
+ onDelete: "set null",
+ }),
+ vendorId: integer("vendor_id").references(() => techVendors.id, {
+ onDelete: "set null",
+ }),
+ isVendorUpload: boolean("is_vendor_upload").default(true), // 벤더가 업로드한 파일인지
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
// 타입 정의
export type TechSalesVendorQuotations =
@@ -390,6 +452,7 @@ export const techSalesVendorQuotationsRelations = relations(techSalesVendorQuota
// 첨부파일 관계
attachments: many(techSalesRfqCommentAttachments),
+ quotationAttachments: many(techSalesVendorQuotationAttachments),
}));
export const techSalesAttachmentsRelations = relations(techSalesAttachments, ({ one }) => ({
@@ -474,4 +537,26 @@ export const techSalesRfqCommentAttachmentsRelations = relations(techSalesRfqCom
fields: [techSalesRfqCommentAttachments.vendorId],
references: [techVendors.id],
}),
+}));
+
+// 기술영업 벤더 견적서 첨부파일 relations
+export const techSalesVendorQuotationAttachmentsRelations = relations(techSalesVendorQuotationAttachments, ({ one }) => ({
+ // 견적서 관계
+ quotation: one(techSalesVendorQuotations, {
+ fields: [techSalesVendorQuotationAttachments.quotationId],
+ references: [techSalesVendorQuotations.id],
+ }),
+
+ // 업로드한 사용자 관계
+ uploadedByUser: one(users, {
+ fields: [techSalesVendorQuotationAttachments.uploadedBy],
+ references: [users.id],
+ relationName: "techSalesQuotationAttachmentUploadedBy",
+ }),
+
+ // 벤더 관계
+ vendor: one(techVendors, {
+ fields: [techSalesVendorQuotationAttachments.vendorId],
+ references: [techVendors.id],
+ }),
})); \ No newline at end of file
diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts
index 62f0f0ef..572b468d 100644
--- a/lib/evaluation-target-list/service.ts
+++ b/lib/evaluation-target-list/service.ts
@@ -1,13 +1,13 @@
'use server'
-import { and, or, desc, asc, ilike, eq, isNull, sql, count } from "drizzle-orm";
+import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm";
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers";
import { filterColumns } from "@/lib/filter-columns";
import db from "@/db/db";
-import {
- evaluationTargets,
- evaluationTargetReviewers,
+import {
+ evaluationTargets,
+ evaluationTargetReviewers,
evaluationTargetReviews,
users,
vendors,
@@ -21,7 +21,9 @@ import {
} from "@/db/schema";
import { GetEvaluationTargetsSchema } from "./validation";
import { PgTransaction } from "drizzle-orm/pg-core";
-
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { sendEmail } from "../mail/sendEmail";
export async function selectEvaluationTargetsFromView(
tx: PgTransaction<any, any, any>,
@@ -52,7 +54,7 @@ export async function countEvaluationTargetsFromView(
.select({ count: count() })
.from(evaluationTargetsWithDepartments)
.where(where);
-
+
return res[0]?.count ?? 0;
}
@@ -102,9 +104,9 @@ export async function getEvaluationTargets(input: GetEvaluationTargetsSchema) {
// 정렬 (View 테이블 기준)
const orderBy = input.sort.length > 0
? input.sort.map((item) => {
- const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments];
- return item.desc ? desc(column) : asc(column);
- })
+ const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments];
+ return item.desc ? desc(column) : asc(column);
+ })
: [desc(evaluationTargetsWithDepartments.createdAt)];
// 데이터 조회 - View 테이블 사용
@@ -196,7 +198,7 @@ export interface CreateEvaluationTargetInput {
// service.ts 파일의 CreateEvaluationTargetInput 타입 수정
export interface CreateEvaluationTargetInput {
evaluationYear: number
- division: "OCEAN" | "SHIPYARD"
+ division: "PLANT" | "SHIP"
vendorId: number
materialType: "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK"
adminComment?: string
@@ -216,17 +218,16 @@ export async function createEvaluationTarget(
input: CreateEvaluationTargetInput,
createdBy: number
) {
-
- console.log(input,"input")
+ console.log(input, "input")
try {
return await db.transaction(async (tx) => {
- // 벤더 정보 조회 (기존과 동일)
+ // 벤더 정보 조회
const vendor = await tx
.select({
id: vendors.id,
vendorCode: vendors.vendorCode,
vendorName: vendors.vendorName,
- country: vendors.country,
+ country: vendors.country,
})
.from(vendors)
.where(eq(vendors.id, input.vendorId))
@@ -238,7 +239,7 @@ export async function createEvaluationTarget(
const vendorInfo = vendor[0];
- // 중복 체크 (기존과 동일)
+ // 중복 체크
const existing = await tx
.select({ id: evaluationTargets.id })
.from(evaluationTargets)
@@ -255,51 +256,57 @@ export async function createEvaluationTarget(
throw new Error("이미 동일한 평가 대상이 존재합니다.");
}
- // 평가 대상 생성 (기존과 동일)
+ // 🔧 수정: 타입 추론 문제 해결
+ const targetValues: typeof evaluationTargets.$inferInsert = {
+ evaluationYear: input.evaluationYear,
+ division: input.division,
+ vendorId: input.vendorId,
+ vendorCode: vendorInfo.vendorCode ?? '',
+ vendorName: vendorInfo.vendorName,
+ domesticForeign: vendorInfo.country === 'KR' ? 'DOMESTIC' : 'FOREIGN',
+ materialType: input.materialType,
+ status: 'PENDING',
+ adminComment: input.adminComment,
+ adminUserId: createdBy,
+ ldClaimCount: input.ldClaimCount ?? 0,
+ // 🔧 수정: decimal 타입은 숫자로 처리
+ ldClaimAmount: input.ldClaimAmount?.toString() ?? '0',
+ ldClaimCurrency: input.ldClaimCurrency ?? 'KRW',
+ }
+
+ console.log(targetValues)
+
+ // 평가 대상 생성
const newEvaluationTarget = await tx
.insert(evaluationTargets)
- .values({
- evaluationYear: input.evaluationYear,
- division: input.division,
- vendorId: input.vendorId,
- vendorCode: vendorInfo.vendorCode || "",
- vendorName: vendorInfo.vendorName,
- domesticForeign: vendorInfo.country === "KR" ? "DOMESTIC" : "FOREIGN",
- materialType: input.materialType,
- status: "PENDING",
- adminComment: input.adminComment,
- adminUserId: createdBy,
- ldClaimCount: input.ldClaimCount || 0,
- ldClaimAmount: input.ldClaimAmount?.toString() || "0",
- ldClaimCurrency: input.ldClaimCurrency || "KRW",
- })
+ .values(targetValues)
.returning({ id: evaluationTargets.id });
const evaluationTargetId = newEvaluationTarget[0].id;
- // ✅ 담당자들 지정 (departmentNameFrom 추가)
+ // 담당자들 지정
if (input.reviewers && input.reviewers.length > 0) {
- // 담당자들의 부서 정보 조회
const reviewerIds = input.reviewers.map(r => r.reviewerUserId);
+
+ // 🔧 수정: SQL 배열 처리 개선
const reviewerInfos = await tx
.select({
id: users.id,
- departmentName: users.departmentName, // users 테이블에 부서명 필드가 있다고 가정
})
.from(users)
- .where(sql`${users.id} = ANY(${reviewerIds})`);
-
- const reviewerAssignments = input.reviewers.map((reviewer) => {
- const reviewerInfo = reviewerInfos.find(info => info.id === reviewer.reviewerUserId);
-
- return {
- evaluationTargetId,
- departmentCode: reviewer.departmentCode,
- departmentNameFrom: reviewerInfo?.departmentName || null, // ✅ 실제 부서명 저장
- reviewerUserId: reviewer.reviewerUserId,
- assignedBy: createdBy,
- };
- });
+ .where(inArray(users.id, reviewerIds)); // sql 대신 inArray 사용
+
+ const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] =
+ input.reviewers.map(r => {
+ const info = reviewerInfos.find(i => i.id === r.reviewerUserId);
+ return {
+ evaluationTargetId,
+ departmentCode: r.departmentCode,
+ departmentNameFrom: info?.departmentName ?? "TEST 부서",
+ reviewerUserId: r.reviewerUserId,
+ assignedBy: createdBy,
+ };
+ });
await tx.insert(evaluationTargetReviewers).values(reviewerAssignments);
}
@@ -319,6 +326,253 @@ export async function createEvaluationTarget(
}
}
+//업데이트 입력 타입 정의
+export interface UpdateEvaluationTargetInput {
+ id: number
+ adminComment?: string
+ consolidatedComment?: string
+ ldClaimCount?: number
+ ldClaimAmount?: number
+ ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY"
+ consensusStatus?: boolean | null
+ orderIsApproved?: boolean | null
+ procurementIsApproved?: boolean | null
+ qualityIsApproved?: boolean | null
+ designIsApproved?: boolean | null
+ csIsApproved?: boolean | null
+ // 담당자 이메일 변경
+ orderReviewerEmail?: string
+ procurementReviewerEmail?: string
+ qualityReviewerEmail?: string
+ designReviewerEmail?: string
+ csReviewerEmail?: string
+}
+
+export interface UpdateEvaluationTargetInput {
+ id: number
+ // 기본 정보
+ adminComment?: string
+ consolidatedComment?: string
+ ldClaimCount?: number
+ ldClaimAmount?: number
+ ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY"
+ consensusStatus?: boolean | null
+
+ // 각 부서별 평가 결과
+ orderIsApproved?: boolean | null
+ procurementIsApproved?: boolean | null
+ qualityIsApproved?: boolean | null
+ designIsApproved?: boolean | null
+ csIsApproved?: boolean | null
+
+ // 담당자 이메일 (사용자 ID로 변환됨)
+ orderReviewerEmail?: string
+ procurementReviewerEmail?: string
+ qualityReviewerEmail?: string
+ designReviewerEmail?: string
+ csReviewerEmail?: string
+}
+
+export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) {
+ console.log(input, "update input")
+
+ try {
+ const session = await auth()
+ if (!session?.user) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ return await db.transaction(async (tx) => {
+ // 평가 대상 존재 확인
+ const existing = await tx
+ .select({ id: evaluationTargets.id })
+ .from(evaluationTargets)
+ .where(eq(evaluationTargets.id, input.id))
+ .limit(1)
+
+ if (!existing.length) {
+ throw new Error("평가 대상을 찾을 수 없습니다.")
+ }
+
+ // 1. 기본 정보 업데이트
+ const updateFields: Partial<typeof evaluationTargets.$inferInsert> = {}
+
+ if (input.adminComment !== undefined) {
+ updateFields.adminComment = input.adminComment
+ }
+ if (input.consolidatedComment !== undefined) {
+ updateFields.consolidatedComment = input.consolidatedComment
+ }
+ if (input.ldClaimCount !== undefined) {
+ updateFields.ldClaimCount = input.ldClaimCount
+ }
+ if (input.ldClaimAmount !== undefined) {
+ updateFields.ldClaimAmount = input.ldClaimAmount.toString()
+ }
+ if (input.ldClaimCurrency !== undefined) {
+ updateFields.ldClaimCurrency = input.ldClaimCurrency
+ }
+ if (input.consensusStatus !== undefined) {
+ updateFields.consensusStatus = input.consensusStatus
+ }
+
+ // 기본 정보가 있으면 업데이트
+ if (Object.keys(updateFields).length > 0) {
+ updateFields.updatedAt = new Date()
+
+ await tx
+ .update(evaluationTargets)
+ .set(updateFields)
+ .where(eq(evaluationTargets.id, input.id))
+ }
+
+ // 2. 담당자 정보 업데이트
+ const reviewerUpdates = [
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, email: input.orderReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, email: input.procurementReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, email: input.qualityReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, email: input.designReviewerEmail },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, email: input.csReviewerEmail },
+ ]
+
+ for (const update of reviewerUpdates) {
+ if (update.email !== undefined) {
+ // 기존 담당자 제거
+ await tx
+ .delete(evaluationTargetReviewers)
+ .where(
+ and(
+ eq(evaluationTargetReviewers.evaluationTargetId, input.id),
+ eq(evaluationTargetReviewers.departmentCode, update.departmentCode)
+ )
+ )
+
+ // 새 담당자 추가 (이메일이 있는 경우만)
+ if (update.email) {
+ // 이메일로 사용자 ID 조회
+ const user = await tx
+ .select({ id: users.id })
+ .from(users)
+ .where(eq(users.email, update.email))
+ .limit(1)
+
+ if (user.length > 0) {
+ await tx
+ .insert(evaluationTargetReviewers)
+ .values({
+ evaluationTargetId: input.id,
+ departmentCode: update.departmentCode,
+ reviewerUserId: user[0].id,
+ assignedBy: session.user.id,
+ })
+ }
+ }
+ }
+ }
+
+ // 3. 평가 결과 업데이트
+ const reviewUpdates = [
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, isApproved: input.orderIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, isApproved: input.procurementIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, isApproved: input.qualityIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, isApproved: input.designIsApproved },
+ { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, isApproved: input.csIsApproved },
+ ]
+
+ for (const review of reviewUpdates) {
+ if (review.isApproved !== undefined) {
+ // 해당 부서의 담당자 조회
+ const reviewer = await tx
+ .select({
+ reviewerUserId: evaluationTargetReviewers.reviewerUserId
+ })
+ .from(evaluationTargetReviewers)
+ .where(
+ and(
+ eq(evaluationTargetReviewers.evaluationTargetId, input.id),
+ eq(evaluationTargetReviewers.departmentCode, review.departmentCode)
+ )
+ )
+ .limit(1)
+
+ if (reviewer.length > 0) {
+ // 기존 평가 결과 삭제
+ await tx
+ .delete(evaluationTargetReviews)
+ .where(
+ and(
+ eq(evaluationTargetReviews.evaluationTargetId, input.id),
+ eq(evaluationTargetReviews.reviewerUserId, reviewer[0].reviewerUserId)
+ )
+ )
+
+ // 새 평가 결과 추가 (null이 아닌 경우만)
+ if (review.isApproved !== null) {
+ await tx
+ .insert(evaluationTargetReviews)
+ .values({
+ evaluationTargetId: input.id,
+ reviewerUserId: reviewer[0].reviewerUserId,
+ departmentCode: review.departmentCode,
+ isApproved: review.isApproved,
+ reviewedAt: new Date(),
+ })
+ }
+ }
+ }
+ }
+
+ // 4. 의견 일치 상태 및 전체 상태 자동 계산
+ const currentReviews = await tx
+ .select({
+ isApproved: evaluationTargetReviews.isApproved,
+ departmentCode: evaluationTargetReviews.departmentCode,
+ })
+ .from(evaluationTargetReviews)
+ .where(eq(evaluationTargetReviews.evaluationTargetId, input.id))
+
+ console.log("Current reviews:", currentReviews)
+
+ // 최소 3개 부서에서 평가가 완료된 경우 의견 일치 상태 계산
+ if (currentReviews.length >= 3) {
+ const approvals = currentReviews.map(r => r.isApproved)
+ const allApproved = approvals.every(approval => approval === true)
+ const allRejected = approvals.every(approval => approval === false)
+ const hasConsensus = allApproved || allRejected
+
+ let newStatus: "PENDING" | "CONFIRMED" | "EXCLUDED" = "PENDING"
+ if (hasConsensus) {
+ newStatus = allApproved ? "CONFIRMED" : "EXCLUDED"
+ }
+
+ console.log("Auto-updating status:", { hasConsensus, newStatus, approvals })
+
+ await tx
+ .update(evaluationTargets)
+ .set({
+ consensusStatus: hasConsensus,
+ status: newStatus,
+ confirmedAt: hasConsensus ? new Date() : null,
+ confirmedBy: hasConsensus ? session.user.id : null,
+ updatedAt: new Date()
+ })
+ .where(eq(evaluationTargets.id, input.id))
+ }
+
+ return {
+ success: true,
+ message: "평가 대상이 성공적으로 수정되었습니다.",
+ }
+ })
+ } catch (error) {
+ console.error("Error updating evaluation target:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "평가 대상 수정 중 오류가 발생했습니다.",
+ }
+ }
+}
+
// 담당자 목록 조회 시 부서 정보도 함께 반환
export async function getAvailableReviewers(departmentCode?: string) {
try {
@@ -358,9 +612,9 @@ export async function getAvailableVendors(search?: string) {
// 검색어가 있으면 적용
search
? or(
- ilike(vendors.vendorCode, `%${search}%`),
- ilike(vendors.vendorName, `%${search}%`)
- )
+ ilike(vendors.vendorCode, `%${search}%`),
+ ilike(vendors.vendorName, `%${search}%`)
+ )
: undefined
)
)
@@ -392,4 +646,266 @@ export async function getDepartmentInfo() {
key,
};
});
+}
+
+
+export async function confirmEvaluationTargets(targetIds: number[]) {
+ try {
+ const session = await getServerSession(authOptions)
+
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ // 확정 가능한 대상들 확인 (PENDING 상태이면서 consensusStatus가 true인 것들)
+ const eligibleTargets = await tx
+ .select()
+ .from(evaluationTargets)
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING"),
+ eq(evaluationTargets.consensusStatus, true)
+ )
+ )
+
+ if (eligibleTargets.length === 0) {
+ throw new Error("확정 가능한 평가 대상이 없습니다. (의견 일치 상태인 대기중 항목만 확정 가능)")
+ }
+
+ // 상태를 CONFIRMED로 변경
+ const confirmedTargetIds = eligibleTargets.map(target => target.id)
+ await tx
+ .update(evaluationTargets)
+ .set({
+ status: "CONFIRMED",
+ confirmedAt: new Date(),
+ confirmedBy: Number(session.user.id),
+ updatedAt: new Date()
+ })
+ .where(inArray(evaluationTargets.id, confirmedTargetIds))
+
+ return confirmedTargetIds
+ })
+
+
+ return {
+ success: true,
+ message: `${targetIds.length}개 평가 대상이 확정되었습니다.`,
+ confirmedCount: targetIds.length
+ }
+
+ } catch (error) {
+ console.error("Error confirming evaluation targets:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "확정 처리 중 오류가 발생했습니다."
+ }
+ }
+}
+
+export async function excludeEvaluationTargets(targetIds: number[]) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ // 제외 가능한 대상들 확인 (PENDING 상태인 것들)
+ const eligibleTargets = await tx
+ .select()
+ .from(evaluationTargets)
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING")
+ )
+ )
+
+ if (eligibleTargets.length === 0) {
+ throw new Error("제외 가능한 평가 대상이 없습니다. (대기중 상태인 항목만 제외 가능)")
+ }
+
+ // 상태를 EXCLUDED로 변경
+ const excludedTargetIds = eligibleTargets.map(target => target.id)
+ await tx
+ .update(evaluationTargets)
+ .set({
+ status: "EXCLUDED",
+ updatedAt: new Date()
+ })
+ .where(inArray(evaluationTargets.id, excludedTargetIds))
+
+ return excludedTargetIds
+ })
+
+
+ return {
+ success: true,
+ message: `${targetIds.length}개 평가 대상이 제외되었습니다.`,
+ excludedCount: targetIds.length
+ }
+
+ } catch (error) {
+ console.error("Error excluding evaluation targets:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "제외 처리 중 오류가 발생했습니다."
+ }
+ }
+}
+
+export async function requestEvaluationReview(targetIds: number[], message?: string) {
+ try {
+ const session = await auth()
+ if (!session?.user) {
+ return { success: false, error: "인증이 필요합니다." }
+ }
+
+ if (targetIds.length === 0) {
+ return { success: false, error: "선택된 평가 대상이 없습니다." }
+ }
+
+ // 선택된 평가 대상들과 담당자 정보 조회
+ const targetsWithReviewers = await db
+ .select({
+ id: evaluationTargets.id,
+ vendorCode: evaluationTargets.vendorCode,
+ vendorName: evaluationTargets.vendorName,
+ materialType: evaluationTargets.materialType,
+ evaluationYear: evaluationTargets.evaluationYear,
+ status: evaluationTargets.status,
+ reviewerEmail: users.email,
+ reviewerName: users.name,
+ departmentCode: evaluationTargetReviewers.departmentCode,
+ departmentName: evaluationTargetReviewers.departmentNameFrom,
+ })
+ .from(evaluationTargets)
+ .leftJoin(
+ evaluationTargetReviewers,
+ eq(evaluationTargets.id, evaluationTargetReviewers.evaluationTargetId)
+ )
+ .leftJoin(
+ users,
+ eq(evaluationTargetReviewers.reviewerUserId, users.id)
+ )
+ .where(
+ and(
+ inArray(evaluationTargets.id, targetIds),
+ eq(evaluationTargets.status, "PENDING")
+ )
+ )
+
+ if (targetsWithReviewers.length === 0) {
+ return { success: false, error: "의견 요청 가능한 평가 대상이 없습니다." }
+ }
+
+ // 평가 대상별로 그룹화
+ const targetGroups = targetsWithReviewers.reduce((acc, item) => {
+ if (!acc[item.id]) {
+ acc[item.id] = {
+ id: item.id,
+ vendorCode: item.vendorCode,
+ vendorName: item.vendorName,
+ materialType: item.materialType,
+ evaluationYear: item.evaluationYear,
+ reviewers: []
+ }
+ }
+
+ if (item.reviewerEmail) {
+ acc[item.id].reviewers.push({
+ email: item.reviewerEmail,
+ name: item.reviewerName,
+ departmentCode: item.departmentCode,
+ departmentName: item.departmentName
+ })
+ }
+
+ return acc
+ }, {} as Record<number, any>)
+
+ const targets = Object.values(targetGroups)
+
+ // 모든 담당자 이메일 수집 (중복 제거)
+ const reviewerEmails = new Set<string>()
+ const reviewerInfo = new Map<string, { name: string; departments: string[] }>()
+
+ targets.forEach(target => {
+ target.reviewers.forEach((reviewer: any) => {
+ if (reviewer.email) {
+ reviewerEmails.add(reviewer.email)
+
+ if (!reviewerInfo.has(reviewer.email)) {
+ reviewerInfo.set(reviewer.email, {
+ name: reviewer.name || reviewer.email,
+ departments: []
+ })
+ }
+
+ const info = reviewerInfo.get(reviewer.email)!
+ if (reviewer.departmentName && !info.departments.includes(reviewer.departmentName)) {
+ info.departments.push(reviewer.departmentName)
+ }
+ }
+ })
+ })
+
+ if (reviewerEmails.size === 0) {
+ return { success: false, error: "담당자가 지정되지 않은 평가 대상입니다." }
+ }
+
+ // 각 담당자에게 이메일 발송
+ const emailPromises = Array.from(reviewerEmails).map(email => {
+ const reviewer = reviewerInfo.get(email)!
+
+ return sendEmail({
+ to: email,
+ subject: `벤더 평가 의견 요청 - ${targets.length}건`,
+ template: "evaluation-review-request",
+ context: {
+ requesterName: session.user.name || session.user.email,
+ reviewerName: reviewer.name,
+ targetCount: targets.length,
+ targets: targets.map(target => ({
+ vendorCode: target.vendorCode,
+ vendorName: target.vendorName,
+ materialType: target.materialType,
+ evaluationYear: target.evaluationYear
+ })),
+ message: message || "",
+ reviewUrl: `${process.env.NEXTAUTH_URL}/evaluation-targets`,
+ requestDate: new Date().toLocaleString('ko-KR')
+ }
+ })
+ })
+
+ await Promise.all(emailPromises)
+
+ revalidatePath("/evaluation-targets")
+ return {
+ success: true,
+ message: `${reviewerEmails.size}명의 담당자에게 의견 요청 이메일이 발송되었습니다.`,
+ emailCount: reviewerEmails.size
+ }
+
+ } catch (error) {
+ console.error("Error requesting evaluation review:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다."
+ }
+ }
} \ No newline at end of file
diff --git a/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
new file mode 100644
index 00000000..47af419d
--- /dev/null
+++ b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
@@ -0,0 +1,384 @@
+// evaluation-target-action-dialogs.tsx
+"use client"
+
+import * as React from "react"
+import { Loader2, AlertTriangle, Check, X, MessageSquare } from "lucide-react"
+import { toast } from "sonner"
+
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { Label } from "@/components/ui/label"
+import { Badge } from "@/components/ui/badge"
+
+import {
+ confirmEvaluationTargets,
+ excludeEvaluationTargets,
+ requestEvaluationReview
+} from "../service"
+import { EvaluationTargetWithDepartments } from "@/db/schema"
+
+// ----------------------------------------------------------------
+// 확정 컨펌 다이얼로그
+// ----------------------------------------------------------------
+interface ConfirmTargetsDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ targets: EvaluationTargetWithDepartments[]
+ onSuccess?: () => void
+}
+
+export function ConfirmTargetsDialog({
+ open,
+ onOpenChange,
+ targets,
+ onSuccess
+}: ConfirmTargetsDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ // 확정 가능한 대상들 (consensusStatus가 true인 것들)
+ const confirmableTargets = targets.filter(
+ t => t.status === "PENDING" && t.consensusStatus === true
+ )
+
+ const handleConfirm = async () => {
+ if (confirmableTargets.length === 0) return
+
+ setIsLoading(true)
+ try {
+ const targetIds = confirmableTargets.map(t => t.id)
+ const result = await confirmEvaluationTargets(targetIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ onSuccess?.()
+ onOpenChange(false)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("확정 처리 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle className="flex items-center gap-2">
+ <Check className="h-5 w-5 text-green-600" />
+ 평가 대상 확정
+ </AlertDialogTitle>
+ <AlertDialogDescription asChild>
+ <div className="space-y-3">
+ <p>
+ 선택된 {targets.length}개 항목 중{" "}
+ <span className="font-semibold text-green-600">
+ {confirmableTargets.length}개 항목
+ </span>
+ 을 확정하시겠습니까?
+ </p>
+
+ {confirmableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 의견 일치 상태인 대기중 항목만 확정 가능합니다.
+ ({targets.length - confirmableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+
+ {confirmableTargets.length > 0 && (
+ <div className="max-h-32 overflow-y-auto">
+ <div className="text-sm space-y-1">
+ {confirmableTargets.slice(0, 5).map(target => (
+ <div key={target.id} className="flex items-center gap-2">
+ <Badge variant="outline" className="text-xs">
+ {target.vendorCode}
+ </Badge>
+ <span className="text-xs">{target.vendorName}</span>
+ </div>
+ ))}
+ {confirmableTargets.length > 5 && (
+ <p className="text-xs text-muted-foreground">
+ ...외 {confirmableTargets.length - 5}개
+ </p>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleConfirm}
+ disabled={isLoading || confirmableTargets.length === 0}
+ className="bg-green-600 hover:bg-green-700"
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 확정 ({confirmableTargets.length})
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+}
+
+// ----------------------------------------------------------------
+// 제외 컨펌 다이얼로그
+// ----------------------------------------------------------------
+interface ExcludeTargetsDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ targets: EvaluationTargetWithDepartments[]
+ onSuccess?: () => void
+}
+
+export function ExcludeTargetsDialog({
+ open,
+ onOpenChange,
+ targets,
+ onSuccess
+}: ExcludeTargetsDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ // 제외 가능한 대상들 (PENDING 상태인 것들)
+ const excludableTargets = targets.filter(t => t.status === "PENDING")
+
+ const handleExclude = async () => {
+ if (excludableTargets.length === 0) return
+
+ setIsLoading(true)
+ try {
+ const targetIds = excludableTargets.map(t => t.id)
+ const result = await excludeEvaluationTargets(targetIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ onSuccess?.()
+ onOpenChange(false)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("제외 처리 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle className="flex items-center gap-2">
+ <X className="h-5 w-5 text-red-600" />
+ 평가 대상 제외
+ </AlertDialogTitle>
+ <AlertDialogDescription asChild>
+ <div className="space-y-3">
+ <p>
+ 선택된 {targets.length}개 항목 중{" "}
+ <span className="font-semibold text-red-600">
+ {excludableTargets.length}개 항목
+ </span>
+ 을 제외하시겠습니까?
+ </p>
+
+ {excludableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 대기중 상태인 항목만 제외 가능합니다.
+ ({targets.length - excludableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+
+ {excludableTargets.length > 0 && (
+ <div className="max-h-32 overflow-y-auto">
+ <div className="text-sm space-y-1">
+ {excludableTargets.slice(0, 5).map(target => (
+ <div key={target.id} className="flex items-center gap-2">
+ <Badge variant="outline" className="text-xs">
+ {target.vendorCode}
+ </Badge>
+ <span className="text-xs">{target.vendorName}</span>
+ </div>
+ ))}
+ {excludableTargets.length > 5 && (
+ <p className="text-xs text-muted-foreground">
+ ...외 {excludableTargets.length - 5}개
+ </p>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleExclude}
+ disabled={isLoading || excludableTargets.length === 0}
+ className="bg-red-600 hover:bg-red-700"
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 제외 ({excludableTargets.length})
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+}
+
+// ----------------------------------------------------------------
+// 의견 요청 다이얼로그
+// ----------------------------------------------------------------
+interface RequestReviewDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ targets: EvaluationTargetWithDepartments[]
+ onSuccess?: () => void
+}
+
+export function RequestReviewDialog({
+ open,
+ onOpenChange,
+ targets,
+ onSuccess
+}: RequestReviewDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [message, setMessage] = React.useState("")
+
+ // 의견 요청 가능한 대상들 (PENDING 상태인 것들)
+ const reviewableTargets = targets.filter(t => t.status === "PENDING")
+
+ // 담당자 이메일들 수집
+ const reviewerEmails = React.useMemo(() => {
+ const emails = new Set<string>()
+ reviewableTargets.forEach(target => {
+ if (target.orderReviewerEmail) emails.add(target.orderReviewerEmail)
+ if (target.procurementReviewerEmail) emails.add(target.procurementReviewerEmail)
+ if (target.qualityReviewerEmail) emails.add(target.qualityReviewerEmail)
+ if (target.designReviewerEmail) emails.add(target.designReviewerEmail)
+ if (target.csReviewerEmail) emails.add(target.csReviewerEmail)
+ })
+ return Array.from(emails)
+ }, [reviewableTargets])
+
+ const handleRequestReview = async () => {
+ if (reviewableTargets.length === 0) return
+
+ setIsLoading(true)
+ try {
+ const targetIds = reviewableTargets.map(t => t.id)
+ const result = await requestEvaluationReview(targetIds, message)
+
+ if (result.success) {
+ toast.success(result.message)
+ onSuccess?.()
+ onOpenChange(false)
+ setMessage("")
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("의견 요청 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <MessageSquare className="h-5 w-5 text-blue-600" />
+ 평가 의견 요청
+ </DialogTitle>
+ <DialogDescription>
+ 선택된 평가 대상에 대한 의견을 담당자들에게 요청합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* 요약 정보 */}
+ <div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
+ <div className="text-sm space-y-1">
+ <p>
+ <span className="font-medium">요청 대상:</span> {reviewableTargets.length}개 평가 항목
+ </p>
+ <p>
+ <span className="font-medium">받는 사람:</span> {reviewerEmails.length}명의 담당자
+ </p>
+ </div>
+ </div>
+
+ {/* 메시지 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="review-message">추가 메시지 (선택사항)</Label>
+ <Textarea
+ id="review-message"
+ placeholder="담당자들에게 전달할 추가 메시지를 입력하세요..."
+ value={message}
+ onChange={(e) => setMessage(e.target.value)}
+ rows={3}
+ />
+ </div>
+
+ {reviewableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 대기중 상태인 항목만 의견 요청 가능합니다.
+ ({targets.length - reviewableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleRequestReview}
+ disabled={isLoading || reviewableTargets.length === 0}
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 의견 요청 발송
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ 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 15837733..fe0b3188 100644
--- a/lib/evaluation-target-list/table/evaluation-target-table.tsx
+++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx
@@ -25,6 +25,7 @@ import { getEvaluationTargetsColumns } from "./evaluation-targets-columns"
import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions"
import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"
import { EvaluationTargetWithDepartments } from "@/db/schema"
+import { EditEvaluationTargetSheet } from "./update-evaluation-target"
interface EvaluationTargetsTableProps {
promises: Promise<[Awaited<ReturnType<typeof getEvaluationTargets>>]>
@@ -40,13 +41,13 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
React.useEffect(() => {
let isMounted = true
-
+
async function fetchStats() {
try {
setIsLoading(true)
setError(null)
const statsData = await getEvaluationTargetsStats(evaluationYear)
-
+
if (isMounted) {
setStats(statsData)
}
@@ -186,45 +187,59 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
export function EvaluationTargetsTable({ promises, evaluationYear, className }: EvaluationTargetsTableProps) {
const [rowAction, setRowAction] = React.useState<DataTableRowAction<EvaluationTargetWithDepartments> | null>(null)
const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
- console.count("E Targets render");
const router = useRouter()
const searchParams = useSearchParams()
const containerRef = React.useRef<HTMLDivElement>(null)
const [containerTop, setContainerTop] = React.useState(0)
+ // ✅ 스크롤 이벤트 throttling으로 성능 최적화
const updateContainerBounds = React.useCallback(() => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect()
- setContainerTop(rect.top)
+ const newTop = rect.top
+
+ // ✅ 값이 실제로 변경될 때만 상태 업데이트
+ setContainerTop(prevTop => {
+ if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트
+ return newTop
+ }
+ return prevTop
+ })
}
}, [])
+ // ✅ throttle 함수 추가
+ const throttledUpdateBounds = React.useCallback(() => {
+ let timeoutId: NodeJS.Timeout
+ return () => {
+ clearTimeout(timeoutId)
+ timeoutId = setTimeout(updateContainerBounds, 16) // ~60fps
+ }
+ }, [updateContainerBounds])
+
React.useEffect(() => {
updateContainerBounds()
-
+
+ const throttledHandler = throttledUpdateBounds()
+
const handleResize = () => {
updateContainerBounds()
}
-
+
window.addEventListener('resize', handleResize)
- window.addEventListener('scroll', updateContainerBounds)
-
+ window.addEventListener('scroll', throttledHandler) // ✅ throttled 함수 사용
+
return () => {
window.removeEventListener('resize', handleResize)
- window.removeEventListener('scroll', updateContainerBounds)
+ window.removeEventListener('scroll', throttledHandler)
}
- }, [updateContainerBounds])
+ }, [updateContainerBounds, throttledUpdateBounds])
const [promiseData] = React.use(promises)
const tableData = promiseData
- console.log("Evaluation Targets Table Data:", {
- dataLength: tableData.data?.length,
- pageCount: tableData.pageCount,
- total: tableData.total,
- sampleData: tableData.data?.[0]
- })
+ console.log(tableData)
const initialSettings = React.useMemo(() => ({
page: parseInt(searchParams.get('page') || '1'),
@@ -232,7 +247,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }],
filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [],
joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and",
- basicFilters: searchParams.get('basicFilters') ?
+ basicFilters: searchParams.get('basicFilters') ?
JSON.parse(searchParams.get('basicFilters')!) : [],
basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
search: searchParams.get('search') || '',
@@ -259,8 +274,8 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
} = useTablePresets<EvaluationTargetWithDepartments>('evaluation-targets-table', initialSettings)
const columns = React.useMemo(
- () => getEvaluationTargetsColumns(),
- []
+ () => getEvaluationTargetsColumns({ setRowAction }),
+ [setRowAction]
)
const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [
@@ -271,31 +286,41 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
const advancedFilterFields: DataTableAdvancedFilterField<EvaluationTargetWithDepartments>[] = [
{ id: "evaluationYear", label: "평가년도", type: "number" },
- { id: "division", label: "구분", type: "select", options: [
- { label: "해양", value: "OCEAN" },
- { label: "조선", value: "SHIPYARD" },
- ]},
+ {
+ id: "division", label: "구분", type: "select", options: [
+ { label: "해양", value: "OCEAN" },
+ { label: "조선", value: "SHIPYARD" },
+ ]
+ },
{ id: "vendorCode", label: "벤더 코드", type: "text" },
{ id: "vendorName", label: "벤더명", type: "text" },
- { id: "domesticForeign", label: "내외자", type: "select", options: [
- { label: "내자", value: "DOMESTIC" },
- { label: "외자", value: "FOREIGN" },
- ]},
- { id: "materialType", label: "자재구분", type: "select", options: [
- { label: "기자재", value: "EQUIPMENT" },
- { label: "벌크", value: "BULK" },
- { label: "기자재/벌크", value: "EQUIPMENT_BULK" },
- ]},
- { id: "status", label: "상태", type: "select", options: [
- { label: "검토 중", value: "PENDING" },
- { label: "확정", value: "CONFIRMED" },
- { label: "제외", value: "EXCLUDED" },
- ]},
- { id: "consensusStatus", label: "의견 일치", type: "select", options: [
- { label: "의견 일치", value: "true" },
- { label: "의견 불일치", value: "false" },
- { label: "검토 중", value: "null" },
- ]},
+ {
+ id: "domesticForeign", label: "내외자", type: "select", options: [
+ { label: "내자", value: "DOMESTIC" },
+ { label: "외자", value: "FOREIGN" },
+ ]
+ },
+ {
+ id: "materialType", label: "자재구분", type: "select", options: [
+ { label: "기자재", value: "EQUIPMENT" },
+ { label: "벌크", value: "BULK" },
+ { label: "기자재/벌크", value: "EQUIPMENT_BULK" },
+ ]
+ },
+ {
+ id: "status", label: "상태", type: "select", options: [
+ { label: "검토 중", value: "PENDING" },
+ { label: "확정", value: "CONFIRMED" },
+ { label: "제외", value: "EXCLUDED" },
+ ]
+ },
+ {
+ id: "consensusStatus", label: "의견 일치", type: "select", options: [
+ { label: "의견 일치", value: "true" },
+ { label: "의견 불일치", value: "false" },
+ { label: "검토 중", value: "null" },
+ ]
+ },
{ id: "adminComment", label: "관리자 의견", type: "text" },
{ id: "consolidatedComment", label: "종합 의견", type: "text" },
{ id: "confirmedAt", label: "확정일", type: "date" },
@@ -305,17 +330,21 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
const currentSettings = useMemo(() => {
return getCurrentSettings()
}, [getCurrentSettings])
-
+
+ function getColKey<T>(c: ColumnDef<T>): string | undefined {
+ if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string
+ if ("id" in c && c.id) return c.id as string
+ return undefined
+ }
+
const initialState = useMemo(() => {
return {
- sorting: initialSettings.sort.filter(sortItem => {
- const columnExists = columns.some(col => col.accessorKey === sortItem.id || col.id === sortItem.id)
- return columnExists
- }) as any,
+ sorting: initialSettings.sort.filter(s =>
+ columns.some(c => getColKey(c) === s.id)),
columnVisibility: currentSettings.columnVisibility,
columnPinning: currentSettings.pinnedColumns,
}
- }, [currentSettings, initialSettings.sort, columns])
+ }, [columns, currentSettings, initialSettings.sort])
const { table } = useDataTable({
data: tableData.data,
@@ -349,12 +378,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
return (
<>
{/* Filter Panel */}
- <div
+ <div
className={cn(
"fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
)}
- style={{
+ style={{
width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
top: `${containerTop}px`,
height: `calc(100vh - ${containerTop}px)`
@@ -362,7 +391,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
>
<div className="h-full">
<EvaluationTargetFilterSheet
- isOpen={isFilterPanelOpen}
+ isOpen={isFilterPanelOpen}
onClose={() => setIsFilterPanelOpen(false)}
onSearch={handleSearch}
isLoading={false}
@@ -371,12 +400,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
</div>
{/* Main Content Container */}
- <div
+ <div
ref={containerRef}
className={cn("relative w-full overflow-hidden", className)}
>
<div className="flex w-full h-full">
- <div
+ <div
className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
style={{
width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%',
@@ -386,14 +415,14 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
{/* Header Bar */}
<div className="flex items-center justify-between p-4 bg-background shrink-0">
<div className="flex items-center gap-3">
- <Button
- variant="outline"
- size="sm"
+ <Button
+ variant="outline"
+ size="sm"
type='button'
onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
className="flex items-center shadow-sm"
>
- {isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/>}
+ {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
{getActiveBasicFilterCount() > 0 && (
<span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
{getActiveBasicFilterCount()}
@@ -401,7 +430,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
)}
</Button>
</div>
-
+
<div className="text-sm text-muted-foreground">
{tableData && (
<span>총 {tableData.total || tableData.data.length}건</span>
@@ -437,11 +466,18 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
onSetDefaultPreset={setDefaultPreset}
onRenamePreset={renamePreset}
/>
-
+
<EvaluationTargetsTableToolbarActions table={table} />
</div>
</DataTableAdvancedToolbar>
</DataTable>
+
+ <EditEvaluationTargetSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ evaluationTarget={rowAction?.row.original ?? null}
+ />
+
</div>
</div>
</div>
diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
index b1e19434..93807ef9 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
@@ -7,6 +7,11 @@ import { Button } from "@/components/ui/button";
import { Pencil, Eye, MessageSquare, Check, X } from "lucide-react";
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
import { EvaluationTargetWithDepartments } from "@/db/schema";
+import { EditEvaluationTargetSheet } from "./update-evaluation-target";
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EvaluationTargetWithDepartments> | null>>;
+}
// 상태별 색상 매핑
const getStatusBadgeVariant = (status: string) => {
@@ -36,8 +41,8 @@ const getConsensusBadge = (consensusStatus: boolean | null) => {
// 구분 배지
const getDivisionBadge = (division: string) => {
return (
- <Badge variant={division === "OCEAN" ? "default" : "secondary"}>
- {division === "OCEAN" ? "해양" : "조선"}
+ <Badge variant={division === "PLANT" ? "default" : "secondary"}>
+ {division === "PLANT" ? "해양" : "조선"}
</Badge>
);
};
@@ -46,7 +51,7 @@ const getDivisionBadge = (division: string) => {
const getMaterialTypeBadge = (materialType: string) => {
const typeMap = {
EQUIPMENT: "기자재",
- BULK: "벌크",
+ BULK: "벌크",
EQUIPMENT_BULK: "기자재/벌크"
};
return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>;
@@ -61,8 +66,23 @@ const getDomesticForeignBadge = (domesticForeign: string) => {
);
};
-export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDepartments>[] {
+// 평가 상태 배지
+const getApprovalBadge = (isApproved: boolean | null) => {
+ if (isApproved === null) {
+ return <Badge variant="outline" className="text-xs">대기중</Badge>;
+ }
+ if (isApproved === true) {
+ return <Badge variant="default" className="bg-green-600 text-xs">승인</Badge>;
+ }
+ return <Badge variant="destructive" className="text-xs">거부</Badge>;
+};
+
+export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] {
return [
+ // ═══════════════════════════════════════════════════════════════
+ // 기본 정보
+ // ═══════════════════════════════════════════════════════════════
+
// Checkbox
{
id: "select",
@@ -102,46 +122,6 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep
cell: ({ row }) => getDivisionBadge(row.getValue("division")),
size: 80,
},
-
- // ░░░ 벤더 코드 ░░░
- {
- accessorKey: "vendorCode",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />,
- cell: ({ row }) => (
- <span className="font-mono text-sm">{row.getValue("vendorCode")}</span>
- ),
- size: 120,
- },
-
- // ░░░ 벤더명 ░░░
- {
- accessorKey: "vendorName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
- cell: ({ row }) => (
- <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}>
- {row.getValue("vendorName") as string}
- </div>
- ),
- size: 200,
- },
-
- // ░░░ 내외자 ░░░
- {
- accessorKey: "domesticForeign",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
- cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")),
- size: 80,
- },
-
- // ░░░ 자재구분 ░░░
- {
- accessorKey: "materialType",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
- cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")),
- size: 120,
- },
-
- // ░░░ 상태 ░░░
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />,
@@ -161,6 +141,54 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep
size: 100,
},
+ // ░░░ 벤더 코드 ░░░
+
+ {
+ header: "협력업체 정보",
+ columns: [
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />,
+ cell: ({ row }) => (
+ <span className="font-mono text-sm">{row.getValue("vendorCode")}</span>
+ ),
+ size: 120,
+ },
+
+ // ░░░ 벤더명 ░░░
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
+ cell: ({ row }) => (
+ <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}>
+ {row.getValue("vendorName") as string}
+ </div>
+ ),
+ size: 200,
+ },
+
+ // ░░░ 내외자 ░░░
+ {
+ accessorKey: "domesticForeign",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
+ cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")),
+ size: 80,
+ },
+
+ ]
+ },
+
+ // ░░░ 자재구분 ░░░
+ {
+ accessorKey: "materialType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
+ cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")),
+ size: 120,
+ },
+
+ // ░░░ 상태 ░░░
+
+
// ░░░ 의견 일치 여부 ░░░
{
accessorKey: "consensusStatus",
@@ -169,56 +197,235 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep
size: 100,
},
- // ░░░ 담당자 현황 ░░░
+ // ═══════════════════════════════════════════════════════════════
+ // 주문 부서 그룹
+ // ═══════════════════════════════════════════════════════════════
{
- id: "reviewers",
- header: "담당자 현황",
- cell: ({ row }) => {
- const reviewers = row.original.reviewers || [];
- const totalReviewers = reviewers.length;
- const completedReviews = reviewers.filter(r => r.review?.isApproved !== null).length;
- const approvedReviews = reviewers.filter(r => r.review?.isApproved === true).length;
-
- return (
- <div className="flex items-center gap-2">
- <div className="text-xs">
- <span className="text-green-600 font-medium">{approvedReviews}</span>
- <span className="text-muted-foreground">/{completedReviews}</span>
- <span className="text-muted-foreground">/{totalReviewers}</span>
- </div>
- {totalReviewers > 0 && (
- <div className="flex gap-1">
- {reviewers.slice(0, 3).map((reviewer, idx) => (
- <div
- key={idx}
- className={`w-2 h-2 rounded-full ${
- reviewer.review?.isApproved === true
- ? "bg-green-500"
- : reviewer.review?.isApproved === false
- ? "bg-red-500"
- : "bg-gray-300"
- }`}
- title={`${reviewer.departmentCode}: ${
- reviewer.review?.isApproved === true
- ? "승인"
- : reviewer.review?.isApproved === false
- ? "거부"
- : "대기중"
- }`}
- />
- ))}
- {totalReviewers > 3 && (
- <span className="text-xs text-muted-foreground">+{totalReviewers - 3}</span>
- )}
+ header: "발주 평가 담당자",
+ columns: [
+ {
+ accessorKey: "orderDepartmentName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
+ cell: ({ row }) => {
+ const departmentName = row.getValue<string>("orderDepartmentName");
+ return departmentName ? (
+ <div className="truncate max-w-[120px]" title={departmentName}>
+ {departmentName}
</div>
- )}
- </div>
- );
- },
- size: 120,
- enableSorting: false,
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "orderReviewerName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ cell: ({ row }) => {
+ const reviewerName = row.getValue<string>("orderReviewerName");
+ return reviewerName ? (
+ <div className="truncate max-w-[100px]" title={reviewerName}>
+ {reviewerName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "orderIsApproved",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
+ cell: ({ row }) => getApprovalBadge(row.getValue("orderIsApproved")),
+ size: 80,
+ },
+ ],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 조달 부서 그룹
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "조달 평가 담당자",
+ columns: [
+ {
+ accessorKey: "procurementDepartmentName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
+ cell: ({ row }) => {
+ const departmentName = row.getValue<string>("procurementDepartmentName");
+ return departmentName ? (
+ <div className="truncate max-w-[120px]" title={departmentName}>
+ {departmentName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "procurementReviewerName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ cell: ({ row }) => {
+ const reviewerName = row.getValue<string>("procurementReviewerName");
+ return reviewerName ? (
+ <div className="truncate max-w-[100px]" title={reviewerName}>
+ {reviewerName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "procurementIsApproved",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
+ cell: ({ row }) => getApprovalBadge(row.getValue("procurementIsApproved")),
+ size: 80,
+ },
+ ],
},
+ // ═══════════════════════════════════════════════════════════════
+ // 품질 부서 그룹
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "품질 평가 담당자",
+ columns: [
+ {
+ accessorKey: "qualityDepartmentName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
+ cell: ({ row }) => {
+ const departmentName = row.getValue<string>("qualityDepartmentName");
+ return departmentName ? (
+ <div className="truncate max-w-[120px]" title={departmentName}>
+ {departmentName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "qualityReviewerName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ cell: ({ row }) => {
+ const reviewerName = row.getValue<string>("qualityReviewerName");
+ return reviewerName ? (
+ <div className="truncate max-w-[100px]" title={reviewerName}>
+ {reviewerName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "qualityIsApproved",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
+ cell: ({ row }) => getApprovalBadge(row.getValue("qualityIsApproved")),
+ size: 80,
+ },
+ ],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 설계 부서 그룹
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "설계 평가 담당자",
+ columns: [
+ {
+ accessorKey: "designDepartmentName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
+ cell: ({ row }) => {
+ const departmentName = row.getValue<string>("designDepartmentName");
+ return departmentName ? (
+ <div className="truncate max-w-[120px]" title={departmentName}>
+ {departmentName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "designReviewerName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ cell: ({ row }) => {
+ const reviewerName = row.getValue<string>("designReviewerName");
+ return reviewerName ? (
+ <div className="truncate max-w-[100px]" title={reviewerName}>
+ {reviewerName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "designIsApproved",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
+ cell: ({ row }) => getApprovalBadge(row.getValue("designIsApproved")),
+ size: 80,
+ },
+ ],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // CS 부서 그룹
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "CS 평가 담당자",
+ columns: [
+ {
+ accessorKey: "csDepartmentName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />,
+ cell: ({ row }) => {
+ const departmentName = row.getValue<string>("csDepartmentName");
+ return departmentName ? (
+ <div className="truncate max-w-[120px]" title={departmentName}>
+ {departmentName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "csReviewerName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />,
+ cell: ({ row }) => {
+ const reviewerName = row.getValue<string>("csReviewerName");
+ return reviewerName ? (
+ <div className="truncate max-w-[100px]" title={reviewerName}>
+ {reviewerName}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "csIsApproved",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />,
+ cell: ({ row }) => getApprovalBadge(row.getValue("csIsApproved")),
+ size: 80,
+ },
+ ],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 관리 정보
+ // ═══════════════════════════════════════════════════════════════
+
// ░░░ 관리자 의견 ░░░
{
accessorKey: "adminComment",
@@ -274,69 +481,47 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep
size: 100,
},
+ // ░░░ 생성일 ░░░
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="생성일" />,
+ cell: ({ row }) => {
+ const createdAt = row.getValue<Date>("createdAt");
+ return createdAt ? (
+ <span className="text-sm">
+ {new Intl.DateTimeFormat("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(createdAt))}
+ </span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+
// ░░░ Actions ░░░
{
id: "actions",
enableHiding: false,
- size: 120,
- minSize: 120,
+ size: 40,
+ minSize: 40,
cell: ({ row }) => {
- const record = row.original;
- const [openDetail, setOpenDetail] = React.useState(false);
- const [openEdit, setOpenEdit] = React.useState(false);
- const [openRequest, setOpenRequest] = React.useState(false);
-
- return (
+ return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-8"
- onClick={() => setOpenDetail(true)}
- aria-label="상세보기"
- title="상세보기"
- >
- <Eye className="size-4" />
- </Button>
-
- <Button
- variant="ghost"
- size="icon"
- className="size-8"
- onClick={() => setOpenEdit(true)}
+ onClick={() => setRowAction({ row, type: "update" })}
aria-label="수정"
title="수정"
>
<Pencil className="size-4" />
</Button>
- <Button
- variant="ghost"
- size="icon"
- className="size-8"
- onClick={() => setOpenRequest(true)}
- aria-label="의견요청"
- title="의견요청"
- >
- <MessageSquare className="size-4" />
- </Button>
-
- {/* TODO: 실제 다이얼로그 컴포넌트들로 교체 */}
- {openDetail && (
- <div onClick={() => setOpenDetail(false)}>
- {/* <EvaluationTargetDetailDialog /> */}
- </div>
- )}
- {openEdit && (
- <div onClick={() => setOpenEdit(false)}>
- {/* <EditEvaluationTargetDialog /> */}
- </div>
- )}
- {openRequest && (
- <div onClick={() => setOpenRequest(false)}>
- {/* <RequestReviewDialog /> */}
- </div>
- )}
</div>
);
},
diff --git a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx
index c14ae83f..502ee974 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx
@@ -44,12 +44,17 @@ const evaluationTargetFilterSchema = z.object({
vendorCode: z.string().optional(),
vendorName: z.string().optional(),
reviewerUserId: z.string().optional(), // 담당자 ID로 필터링
+ orderReviewerName: z.string().optional(), // 주문 검토자명
+ procurementReviewerName: z.string().optional(), // 조달 검토자명
+ qualityReviewerName: z.string().optional(), // 품질 검토자명
+ designReviewerName: z.string().optional(), // 설계 검토자명
+ csReviewerName: z.string().optional(), // CS 검토자명
})
// 옵션 정의
const divisionOptions = [
- { value: "OCEAN", label: "해양" },
- { value: "SHIPYARD", label: "조선" },
+ { value: "PLANT", label: "해양" },
+ { value: "SHIP", label: "조선" },
]
const statusOptions = [
@@ -128,6 +133,11 @@ export function EvaluationTargetFilterSheet({
vendorCode: "",
vendorName: "",
reviewerUserId: "",
+ orderReviewerName: "",
+ procurementReviewerName: "",
+ qualityReviewerName: "",
+ designReviewerName: "",
+ csReviewerName: "",
},
})
@@ -261,6 +271,57 @@ export function EvaluationTargetFilterSheet({
})
}
+ // 새로 추가된 검토자명 필터들
+ if (data.orderReviewerName?.trim()) {
+ newFilters.push({
+ id: "orderReviewerName",
+ value: data.orderReviewerName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.procurementReviewerName?.trim()) {
+ newFilters.push({
+ id: "procurementReviewerName",
+ value: data.procurementReviewerName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.qualityReviewerName?.trim()) {
+ newFilters.push({
+ id: "qualityReviewerName",
+ value: data.qualityReviewerName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.designReviewerName?.trim()) {
+ newFilters.push({
+ id: "designReviewerName",
+ value: data.designReviewerName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.csReviewerName?.trim()) {
+ newFilters.push({
+ id: "csReviewerName",
+ value: data.csReviewerName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
// URL 업데이트
const currentUrl = new URL(window.location.href);
const params = new URLSearchParams(currentUrl.search);
@@ -313,6 +374,11 @@ export function EvaluationTargetFilterSheet({
vendorCode: "",
vendorName: "",
reviewerUserId: "",
+ orderReviewerName: "",
+ procurementReviewerName: "",
+ qualityReviewerName: "",
+ designReviewerName: "",
+ csReviewerName: "",
});
// URL 초기화
@@ -723,6 +789,191 @@ export function EvaluationTargetFilterSheet({
)}
/>
+ {/* 주문 검토자명 */}
+ <FormField
+ control={form.control}
+ name="orderReviewerName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>발주 담당자명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="발주 담당자명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("orderReviewerName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 조달 검토자명 */}
+ <FormField
+ control={form.control}
+ name="procurementReviewerName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>조달 담당자명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="조달 담당자명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("procurementReviewerName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 품질 검토자명 */}
+ <FormField
+ control={form.control}
+ name="qualityReviewerName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>품질 담당자명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="품질 담당자명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("qualityReviewerName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 설계 검토자명 */}
+ <FormField
+ control={form.control}
+ name="designReviewerName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>설계 담당자명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="설계 담당자명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("designReviewerName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* CS 검토자명 */}
+ <FormField
+ control={form.control}
+ name="csReviewerName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>CS 담당자명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="CS 담당자명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("csReviewerName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
</div>
</div>
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 3fb47771..9043c588 100644
--- a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
+++ b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx
@@ -24,7 +24,13 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ManualCreateEvaluationTargetDialog } from "./manual-create-evaluation-target-dialog"
+import {
+ ConfirmTargetsDialog,
+ ExcludeTargetsDialog,
+ RequestReviewDialog
+} from "./evaluation-target-action-dialogs"
import { EvaluationTargetWithDepartments } from "@/db/schema"
+import { exportTableToExcel } from "@/lib/export"
interface EvaluationTargetsTableToolbarActionsProps {
table: Table<EvaluationTargetWithDepartments>
@@ -37,6 +43,9 @@ export function EvaluationTargetsTableToolbarActions({
}: EvaluationTargetsTableToolbarActionsProps) {
const [isLoading, setIsLoading] = React.useState(false)
const [manualCreateDialogOpen, setManualCreateDialogOpen] = React.useState(false)
+ const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
+ const [excludeDialogOpen, setExcludeDialogOpen] = React.useState(false)
+ const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false)
const router = useRouter()
// 선택된 행들
@@ -91,84 +100,12 @@ export function EvaluationTargetsTableToolbarActions({
}
// ----------------------------------------------------------------
- // 선택된 항목들 확정
+ // 다이얼로그 성공 핸들러
// ----------------------------------------------------------------
- const handleConfirmSelected = async () => {
- if (!hasSelection || !selectedStats.canConfirm) return
-
- setIsLoading(true)
- try {
- // TODO: 확정 API 호출
- const confirmableTargets = selectedTargets.filter(
- t => t.status === "PENDING" && t.consensusStatus === true
- )
-
- toast.success(`${confirmableTargets.length}개 항목이 확정되었습니다.`)
- table.resetRowSelection()
- router.refresh()
- } catch (error) {
- console.error('Error confirming targets:', error)
- toast.error("확정 처리 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- // ----------------------------------------------------------------
- // 선택된 항목들 제외
- // ----------------------------------------------------------------
- const handleExcludeSelected = async () => {
- if (!hasSelection || !selectedStats.canExclude) return
-
- setIsLoading(true)
- try {
- // TODO: 제외 API 호출
- const excludableTargets = selectedTargets.filter(t => t.status === "PENDING")
-
- toast.success(`${excludableTargets.length}개 항목이 제외되었습니다.`)
- table.resetRowSelection()
- router.refresh()
- } catch (error) {
- console.error('Error excluding targets:', error)
- toast.error("제외 처리 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- // ----------------------------------------------------------------
- // 선택된 항목들 의견 요청
- // ----------------------------------------------------------------
- const handleRequestReview = async () => {
- if (!hasSelection || !selectedStats.canRequestReview) return
-
- // TODO: 의견 요청 다이얼로그 열기
- toast.info("의견 요청 다이얼로그를 구현해주세요.")
- }
-
- // ----------------------------------------------------------------
- // Excel 내보내기
- // ----------------------------------------------------------------
- const handleExport = () => {
- try {
- // TODO: Excel 내보내기 구현
- toast.success("Excel 파일이 다운로드되었습니다.")
- } catch (error) {
- console.error('Error exporting to Excel:', error)
- toast.error("Excel 내보내기 중 오류가 발생했습니다.")
- }
- }
-
- // ----------------------------------------------------------------
- // 새로고침
- // ----------------------------------------------------------------
- const handleRefresh = () => {
- if (onRefresh) {
- onRefresh()
- } else {
- router.refresh()
- }
- toast.success("데이터가 새로고침되었습니다.")
+ const handleActionSuccess = () => {
+ table.resetRowSelection()
+ onRefresh?.()
+ router.refresh()
}
return (
@@ -204,22 +141,17 @@ export function EvaluationTargetsTableToolbarActions({
<Button
variant="outline"
size="sm"
- onClick={handleExport}
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "vendor-target-list",
+ excludeColumns: ["select", "actions"],
+ })
+ }
className="gap-2"
>
<Download className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">내보내기</span>
</Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={handleRefresh}
- className="gap-2"
- >
- <RefreshCw className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">새로고침</span>
- </Button>
</div>
{/* 선택된 항목 액션 버튼들 */}
@@ -231,7 +163,7 @@ export function EvaluationTargetsTableToolbarActions({
variant="default"
size="sm"
className="gap-2 bg-green-600 hover:bg-green-700"
- onClick={handleConfirmSelected}
+ onClick={() => setConfirmDialogOpen(true)}
disabled={isLoading}
>
<Check className="size-4" aria-hidden="true" />
@@ -247,7 +179,7 @@ export function EvaluationTargetsTableToolbarActions({
variant="destructive"
size="sm"
className="gap-2"
- onClick={handleExcludeSelected}
+ onClick={() => setExcludeDialogOpen(true)}
disabled={isLoading}
>
<X className="size-4" aria-hidden="true" />
@@ -263,7 +195,7 @@ export function EvaluationTargetsTableToolbarActions({
variant="outline"
size="sm"
className="gap-2"
- onClick={handleRequestReview}
+ onClick={() => setReviewDialogOpen(true)}
disabled={isLoading}
>
<MessageSquare className="size-4" aria-hidden="true" />
@@ -282,6 +214,30 @@ export function EvaluationTargetsTableToolbarActions({
onOpenChange={setManualCreateDialogOpen}
/>
+ {/* 확정 컨펌 다이얼로그 */}
+ <ConfirmTargetsDialog
+ open={confirmDialogOpen}
+ onOpenChange={setConfirmDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
+
+ {/* 제외 컨펌 다이얼로그 */}
+ <ExcludeTargetsDialog
+ open={excludeDialogOpen}
+ onOpenChange={setExcludeDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
+
+ {/* 의견 요청 다이얼로그 */}
+ <RequestReviewDialog
+ open={reviewDialogOpen}
+ onOpenChange={setReviewDialogOpen}
+ targets={selectedTargets}
+ onSuccess={handleActionSuccess}
+ />
+
{/* 선택 정보 표시 */}
{hasSelection && (
<div className="text-xs text-muted-foreground">
diff --git a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
index 5704cba1..af369ea6 100644
--- a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
+++ b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
@@ -60,26 +60,7 @@ import {
import { EVALUATION_TARGET_FILTER_OPTIONS, getDefaultEvaluationYear } from "../validation"
import { useSession } from "next-auth/react"
-// 폼 스키마 정의
-const createEvaluationTargetSchema = z.object({
- evaluationYear: z.number().min(2020).max(2030),
- division: z.enum(["OCEAN", "SHIPYARD"]),
- vendorId: z.number().min(1, "벤더를 선택해주세요"),
- materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]),
- adminComment: z.string().optional(),
- // L/D 클레임 정보
- ldClaimCount: z.number().min(0).optional(),
- ldClaimAmount: z.number().min(0).optional(),
- ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(),
- reviewers: z.array(
- z.object({
- departmentCode: z.string(),
- reviewerUserId: z.number().min(1, "담당자를 선택해주세요"),
- })
- ).min(1, "최소 1명의 담당자를 지정해주세요"),
-})
-type CreateEvaluationTargetFormValues = z.infer<typeof createEvaluationTargetSchema>
interface ManualCreateEvaluationTargetDialogProps {
open: boolean
@@ -114,12 +95,49 @@ export function ManualCreateEvaluationTargetDialog({
// 부서 정보 상태
const [departments, setDepartments] = React.useState<Array<{ code: string, name: string, key: string }>>([])
+ // 폼 스키마 정의
+const createEvaluationTargetSchema = z.object({
+ evaluationYear: z.number().min(2020).max(2030),
+ division: z.enum(["PLANT", "SHIP"]),
+ vendorId: z.number().min(0), // 0도 허용, 클라이언트에서 검증
+ materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]),
+ adminComment: z.string().optional(),
+ // L/D 클레임 정보
+ ldClaimCount: z.number().min(0).optional(),
+ ldClaimAmount: z.number().min(0).optional(),
+ ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(),
+ reviewers: z.array(
+ z.object({
+ departmentCode: z.string(),
+ reviewerUserId: z.number(), // min(1) 제거, 나중에 클라이언트에서 필터링
+ })
+ ),
+}).refine((data) => {
+ // 벤더가 선택되어야 함
+ if (data.vendorId === 0) {
+ return false;
+ }
+ // 최소 1명의 담당자가 지정되어야 함 (reviewerUserId > 0)
+ const validReviewers = data.reviewers
+ .filter(r => r.reviewerUserId > 0)
+ .map((r, i) => ({
+ departmentCode: r.departmentCode || departments[i]?.code, // 없으면 보충
+ reviewerUserId: r.reviewerUserId,
+ }));
+ return validReviewers.length > 0;
+}, {
+ message: "벤더를 선택하고 최소 1명의 담당자를 지정해주세요.",
+ path: ["vendorId"]
+})
+type CreateEvaluationTargetFormValues = z.infer<typeof createEvaluationTargetSchema>
+
+
const form = useForm<CreateEvaluationTargetFormValues>({
resolver: zodResolver(createEvaluationTargetSchema),
defaultValues: {
evaluationYear: getDefaultEvaluationYear(),
- division: "OCEAN",
- vendorId: 0,
+ division: "SHIP",
+ vendorId: 0, // 임시로 0, 나중에 검증에서 체크
materialType: "EQUIPMENT",
adminComment: "",
ldClaimCount: 0,
@@ -180,17 +198,12 @@ export function ManualCreateEvaluationTargetDialog({
// 부서 정보가 로드되면 reviewers 기본값 설정
React.useEffect(() => {
if (departments.length > 0 && open) {
- const currentReviewers = form.getValues("reviewers")
-
- // 이미 설정되어 있으면 다시 설정하지 않음
- if (currentReviewers.length === 0) {
- const defaultReviewers = departments.map(dept => ({
- departmentCode: dept.code,
- reviewerUserId: 0,
- }))
- form.setValue('reviewers', defaultReviewers)
- }
- }
+ const defaultReviewers = departments.map(dept => ({
+ departmentCode: dept.code, // ✅ 반드시 포함
+ reviewerUserId: 0,
+ }));
+ form.setValue("reviewers", defaultReviewers, { shouldValidate: false });
+ }
}, [departments, open]) // form 의존성 제거하고 조건 추가
console.log(departments)
@@ -234,7 +247,7 @@ export function ManualCreateEvaluationTargetDialog({
// 폼과 상태 초기화
form.reset({
evaluationYear: getDefaultEvaluationYear(),
- division: "OCEAN",
+ division: "SHIP",
vendorId: 0,
materialType: "EQUIPMENT",
adminComment: "",
@@ -269,7 +282,7 @@ export function ManualCreateEvaluationTargetDialog({
if (!open) {
form.reset({
evaluationYear: getDefaultEvaluationYear(),
- division: "OCEAN",
+ division: "SHIP",
vendorId: 0,
materialType: "EQUIPMENT",
adminComment: "",
@@ -326,24 +339,29 @@ export function ManualCreateEvaluationTargetDialog({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="max-w-lg flex flex-col h-[90vh]">
+ <DialogContent className="max-w-lg h-[90vh] p-0 flex flex-col">
{/* 고정 헤더 */}
- <DialogHeader className="flex-shrink-0">
- <DialogTitle>평가 대상 수동 생성</DialogTitle>
- <DialogDescription>
- 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다.
- </DialogDescription>
- </DialogHeader>
+ <div className="flex-shrink-0 p-6 border-b">
+ <DialogHeader>
+ <DialogTitle>평가 대상 수동 생성</DialogTitle>
+ <DialogDescription>
+ 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다.
+ </DialogDescription>
+ </DialogHeader>
+ </div>
{/* Form을 전체 콘텐츠를 감싸도록 수정 */}
<Form {...form}>
<form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col flex-1"
+ onSubmit={form.handleSubmit(
+ onSubmit,
+ (errors) => console.log('❌ validation errors:', errors)
+ )}
+ className="flex flex-col flex-1 min-h-0"
id="evaluation-target-form"
>
{/* 스크롤 가능한 콘텐츠 영역 */}
- <div className="flex-1 overflow-y-auto">
+ <div className="flex-1 overflow-y-auto p-6">
<div className="space-y-6">
{/* 기본 정보 */}
<Card>
@@ -693,9 +711,14 @@ export function ManualCreateEvaluationTargetDialog({
<CommandItem
value="선택 안함"
onSelect={() => {
- field.onChange(0)
- setReviewerOpens(prev => ({...prev, [department.code]: false}))
- }}
+ // reviewers[index] 전체를 갱신
+ form.setValue(
+ `reviewers.${index}`,
+ { departmentCode: department.code, reviewerUserId: reviewer.id },
+ { shouldValidate: true }
+ );
+ setReviewerOpens(prev => ({ ...prev, [department.code]: false }));
+ }}
>
<Check
className={cn(
@@ -747,22 +770,24 @@ export function ManualCreateEvaluationTargetDialog({
</div>
{/* 고정 버튼 영역 */}
- <div className="flex-shrink-0 flex justify-end gap-3 pt-4 border-t">
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isSubmitting}
- >
- {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- 생성
- </Button>
+ <div className="flex-shrink-0 border-t bg-background p-6">
+ <div className="flex justify-end gap-3">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => handleOpenChange(false)}
+ disabled={isSubmitting}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isSubmitting}
+ >
+ {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 생성
+ </Button>
+ </div>
</div>
</form>
</Form>
diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx
new file mode 100644
index 00000000..0d56addb
--- /dev/null
+++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx
@@ -0,0 +1,760 @@
+"use client"
+
+import * as React from "react"
+import { useRouter } from "next/navigation"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import * as z from "zod"
+import { Check, ChevronsUpDown, Loader2, X } from "lucide-react"
+import { toast } from "sonner"
+import { useSession } from "next-auth/react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { cn } from "@/lib/utils"
+
+import {
+ updateEvaluationTarget,
+ getAvailableReviewers,
+ getDepartmentInfo,
+ type UpdateEvaluationTargetInput,
+} from "../service"
+import { EvaluationTargetWithDepartments } from "@/db/schema"
+
+// 편집 가능한 필드들에 대한 스키마
+const editEvaluationTargetSchema = z.object({
+ adminComment: z.string().optional(),
+ consolidatedComment: z.string().optional(),
+ ldClaimCount: z.number().min(0).optional(),
+ ldClaimAmount: z.number().min(0).optional(),
+ ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(),
+ consensusStatus: z.boolean().nullable().optional(),
+ orderIsApproved: z.boolean().nullable().optional(),
+ procurementIsApproved: z.boolean().nullable().optional(),
+ qualityIsApproved: z.boolean().nullable().optional(),
+ designIsApproved: z.boolean().nullable().optional(),
+ csIsApproved: z.boolean().nullable().optional(),
+ // 담당자 정보 수정
+ orderReviewerEmail: z.string().optional(),
+ procurementReviewerEmail: z.string().optional(),
+ qualityReviewerEmail: z.string().optional(),
+ designReviewerEmail: z.string().optional(),
+ csReviewerEmail: z.string().optional(),
+})
+
+type EditEvaluationTargetFormValues = z.infer<typeof editEvaluationTargetSchema>
+
+interface EditEvaluationTargetSheetProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ evaluationTarget: EvaluationTargetWithDepartments | null
+}
+
+// 권한 타입 정의
+type PermissionLevel = "none" | "department" | "admin"
+
+interface UserPermissions {
+ level: PermissionLevel
+ editableApprovals: string[] // 편집 가능한 approval 필드들
+}
+
+export function EditEvaluationTargetSheet({
+ open,
+ onOpenChange,
+ evaluationTarget,
+}: EditEvaluationTargetSheetProps) {
+ const router = useRouter()
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const { data: session } = useSession()
+
+ // 담당자 관련 상태
+ const [reviewers, setReviewers] = React.useState<Array<{ id: number, name: string, email: string }>>([])
+ const [isLoadingReviewers, setIsLoadingReviewers] = React.useState(false)
+
+ // 각 부서별 담당자 선택 상태
+ const [reviewerSearches, setReviewerSearches] = React.useState<Record<string, string>>({})
+ const [reviewerOpens, setReviewerOpens] = React.useState<Record<string, boolean>>({})
+
+ // 부서 정보 상태
+ const [departments, setDepartments] = React.useState<Array<{ code: string, name: string, key: string }>>([])
+
+ // 사용자 권한 계산
+ const userPermissions = React.useMemo((): UserPermissions => {
+ if (!session?.user || !evaluationTarget) {
+ return { level: "none", editableApprovals: [] }
+ }
+
+ const userEmail = session.user.email
+ const userRole = session.user.role
+
+ // 평가관리자는 모든 권한
+ if (userRole === "평가관리자") {
+ return {
+ level: "admin",
+ editableApprovals: [
+ "orderIsApproved",
+ "procurementIsApproved",
+ "qualityIsApproved",
+ "designIsApproved",
+ "csIsApproved"
+ ]
+ }
+ }
+
+ // 부서별 담당자 권한 확인
+ const editableApprovals: string[] = []
+
+ if (evaluationTarget.orderReviewerEmail === userEmail) {
+ editableApprovals.push("orderIsApproved")
+ }
+ if (evaluationTarget.procurementReviewerEmail === userEmail) {
+ editableApprovals.push("procurementIsApproved")
+ }
+ if (evaluationTarget.qualityReviewerEmail === userEmail) {
+ editableApprovals.push("qualityIsApproved")
+ }
+ if (evaluationTarget.designReviewerEmail === userEmail) {
+ editableApprovals.push("designIsApproved")
+ }
+ if (evaluationTarget.csReviewerEmail === userEmail) {
+ editableApprovals.push("csIsApproved")
+ }
+
+ return {
+ level: editableApprovals.length > 0 ? "department" : "none",
+ editableApprovals
+ }
+ }, [session, evaluationTarget])
+
+ const form = useForm<EditEvaluationTargetFormValues>({
+ resolver: zodResolver(editEvaluationTargetSchema),
+ defaultValues: {
+ adminComment: "",
+ consolidatedComment: "",
+ ldClaimCount: 0,
+ ldClaimAmount: 0,
+ ldClaimCurrency: "KRW",
+ consensusStatus: null,
+ orderIsApproved: null,
+ procurementIsApproved: null,
+ qualityIsApproved: null,
+ designIsApproved: null,
+ csIsApproved: null,
+ orderReviewerEmail: "",
+ procurementReviewerEmail: "",
+ qualityReviewerEmail: "",
+ designReviewerEmail: "",
+ csReviewerEmail: "",
+ },
+ })
+
+ // 부서 정보 로드
+ const loadDepartments = React.useCallback(async () => {
+ try {
+ const departmentList = await getDepartmentInfo()
+ setDepartments(departmentList)
+ } catch (error) {
+ console.error("Error loading departments:", error)
+ toast.error("부서 정보를 불러오는데 실패했습니다.")
+ }
+ }, [])
+
+ // 담당자 목록 로드
+ const loadReviewers = React.useCallback(async () => {
+ setIsLoadingReviewers(true)
+ try {
+ const reviewerList = await getAvailableReviewers()
+ setReviewers(reviewerList)
+ } catch (error) {
+ console.error("Error loading reviewers:", error)
+ toast.error("담당자 목록을 불러오는데 실패했습니다.")
+ } finally {
+ setIsLoadingReviewers(false)
+ }
+ }, [])
+
+ // 시트가 열릴 때 데이터 로드 및 폼 초기화
+ React.useEffect(() => {
+ if (open && evaluationTarget) {
+ loadDepartments()
+ loadReviewers()
+
+ // 폼에 기존 데이터 설정
+ form.reset({
+ adminComment: evaluationTarget.adminComment || "",
+ consolidatedComment: evaluationTarget.consolidatedComment || "",
+ ldClaimCount: evaluationTarget.ldClaimCount || 0,
+ ldClaimAmount: parseFloat(evaluationTarget.ldClaimAmount || "0"),
+ ldClaimCurrency: evaluationTarget.ldClaimCurrency || "KRW",
+ consensusStatus: evaluationTarget.consensusStatus,
+ orderIsApproved: evaluationTarget.orderIsApproved,
+ procurementIsApproved: evaluationTarget.procurementIsApproved,
+ qualityIsApproved: evaluationTarget.qualityIsApproved,
+ designIsApproved: evaluationTarget.designIsApproved,
+ csIsApproved: evaluationTarget.csIsApproved,
+ orderReviewerEmail: evaluationTarget.orderReviewerEmail || "",
+ procurementReviewerEmail: evaluationTarget.procurementReviewerEmail || "",
+ qualityReviewerEmail: evaluationTarget.qualityReviewerEmail || "",
+ designReviewerEmail: evaluationTarget.designReviewerEmail || "",
+ csReviewerEmail: evaluationTarget.csReviewerEmail || "",
+ })
+ }
+ }, [open, evaluationTarget, form])
+
+ // 폼 제출
+ async function onSubmit(data: EditEvaluationTargetFormValues) {
+ if (!evaluationTarget) return
+
+ setIsSubmitting(true)
+ try {
+ const input: UpdateEvaluationTargetInput = {
+ id: evaluationTarget.id,
+ ...data,
+ }
+
+ console.log("Updating evaluation target:", input)
+
+ const result = await updateEvaluationTarget(input)
+
+ if (result.success) {
+ toast.success(result.message || "평가 대상이 성공적으로 수정되었습니다.")
+ onOpenChange(false)
+ router.refresh()
+ } else {
+ toast.error(result.error || "평가 대상 수정에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("Error updating evaluation target:", error)
+ toast.error("평가 대상 수정 중 오류가 발생했습니다.")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ // 시트 닫기 핸들러
+ const handleOpenChange = (open: boolean) => {
+ onOpenChange(open)
+ if (!open) {
+ form.reset()
+ setReviewerSearches({})
+ setReviewerOpens({})
+ }
+ }
+
+ // 담당자 검색 필터링
+ const getFilteredReviewers = (search: string) => {
+ if (!search) return reviewers
+ return reviewers.filter(reviewer =>
+ reviewer.name.toLowerCase().includes(search.toLowerCase()) ||
+ reviewer.email.toLowerCase().includes(search.toLowerCase())
+ )
+ }
+
+ // 필드 편집 권한 확인
+ const canEditField = (fieldName: string): boolean => {
+ if (userPermissions.level === "admin") return true
+ if (userPermissions.level === "none") return false
+
+ // 부서 담당자는 자신의 approval만 편집 가능
+ return userPermissions.editableApprovals.includes(fieldName)
+ }
+
+ // 관리자 전용 필드 확인
+ const canEditAdminFields = (): boolean => {
+ return userPermissions.level === "admin"
+ }
+
+ if (!evaluationTarget) {
+ return null
+ }
+
+ // 권한이 없는 경우
+ if (userPermissions.level === "none") {
+ return (
+ <Sheet open={open} onOpenChange={handleOpenChange}>
+ <SheetContent className="sm:max-w-lg overflow-y-auto">
+ <SheetHeader>
+ <SheetTitle>평가 대상 수정</SheetTitle>
+ <SheetDescription>
+ 권한이 없어 수정할 수 없습니다.
+ </SheetDescription>
+ </SheetHeader>
+ <div className="mt-6 p-4 bg-muted rounded-lg text-center">
+ <p className="text-sm text-muted-foreground">
+ 이 평가 대상을 수정할 권한이 없습니다.
+ </p>
+ </div>
+ </SheetContent>
+ </Sheet>
+ )
+ }
+
+ return (
+ <Sheet open={open} onOpenChange={handleOpenChange}>
+ <SheetContent className="flex flex-col h-full sm:max-w-xl">
+ <SheetHeader className="flex-shrink-0 text-left pb-6">
+ <SheetTitle>평가 대상 수정</SheetTitle>
+ <SheetDescription>
+ 평가 대상 정보를 수정합니다.
+ {userPermissions.level === "department" && (
+ <div className="mt-2 p-2 bg-blue-50 rounded text-sm">
+ <strong>부서 담당자 권한:</strong> 해당 부서의 평가 항목만 수정 가능합니다.
+ </div>
+ )}
+ {userPermissions.level === "admin" && (
+ <div className="mt-2 p-2 bg-green-50 rounded text-sm">
+ <strong>평가관리자 권한:</strong> 모든 항목을 수정할 수 있습니다.
+ </div>
+ )}
+ </SheetDescription>
+ </SheetHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0">
+ {/* 스크롤 가능한 컨텐츠 영역 */}
+ <div className="flex-1 overflow-y-auto pr-2 -mr-2">
+ <div className="space-y-6">
+ {/* 기본 정보 (읽기 전용) */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">기본 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div>
+ <span className="font-medium">평가년도:</span> {evaluationTarget.evaluationYear}
+ </div>
+ <div>
+ <span className="font-medium">구분:</span> {evaluationTarget.division === "PLANT" ? "해양" : "조선"}
+ </div>
+ <div>
+ <span className="font-medium">벤더 코드:</span> {evaluationTarget.vendorCode}
+ </div>
+ <div>
+ <span className="font-medium">벤더명:</span> {evaluationTarget.vendorName}
+ </div>
+ <div>
+ <span className="font-medium">자재구분:</span> {evaluationTarget.materialType}
+ </div>
+ <div>
+ <span className="font-medium">상태:</span> {evaluationTarget.status}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* L/D 클레임 정보 (관리자만) */}
+ {canEditAdminFields() && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">L/D 클레임 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-3 gap-4">
+ <FormField
+ control={form.control}
+ name="ldClaimCount"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>클레임 건수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ {...field}
+ onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="ldClaimAmount"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>클레임 금액</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ step="0.01"
+ {...field}
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="ldClaimCurrency"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>통화단위</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="KRW">KRW (원)</SelectItem>
+ <SelectItem value="USD">USD (달러)</SelectItem>
+ <SelectItem value="EUR">EUR (유로)</SelectItem>
+ <SelectItem value="JPY">JPY (엔)</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 평가 상태 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">평가 상태</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+
+ {/* 의견 일치 여부 (관리자만) */}
+ {canEditAdminFields() && (
+ <FormField
+ control={form.control}
+ name="consensusStatus"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>의견 일치 여부</FormLabel>
+ <Select
+ onValueChange={(value) => {
+ if (value === "null") field.onChange(null)
+ else field.onChange(value === "true")
+ }}
+ value={field.value === null ? "null" : field.value.toString()}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="null">검토 중</SelectItem>
+ <SelectItem value="true">의견 일치</SelectItem>
+ <SelectItem value="false">의견 불일치</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 각 부서별 평가 */}
+ <div className="grid grid-cols-1 gap-4">
+ {[
+ { key: "orderIsApproved", label: "주문 부서 평가", email: evaluationTarget.orderReviewerEmail },
+ { key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail },
+ { key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail },
+ { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail },
+ { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail },
+ ].map(({ key, label, email }) => (
+ <FormField
+ key={key}
+ control={form.control}
+ name={key as keyof EditEvaluationTargetFormValues}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center justify-between">
+ <span>{label}</span>
+ {email && (
+ <span className="text-xs text-muted-foreground">
+ 담당자: {email}
+ </span>
+ )}
+ </FormLabel>
+ <Select
+ onValueChange={(value) => {
+ if (value === "null") field.onChange(null)
+ else field.onChange(value === "true")
+ }}
+ value={field.value === null ? "null" : field.value?.toString()}
+ disabled={!canEditField(key)}
+ >
+ <FormControl>
+ <SelectTrigger className={!canEditField(key) ? "opacity-50" : ""}>
+ <SelectValue />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="null">대기중</SelectItem>
+ <SelectItem value="true">평가대상 맞음</SelectItem>
+ <SelectItem value="false">평가대상 아님</SelectItem>
+ </SelectContent>
+ </Select>
+ {!canEditField(key) && (
+ <p className="text-xs text-muted-foreground">
+ 편집 권한이 없습니다.
+ </p>
+ )}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 의견 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">의견</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 관리자 의견 (관리자만) */}
+ {canEditAdminFields() && (
+ <FormField
+ control={form.control}
+ name="adminComment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>관리자 의견</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="관리자 의견을 입력하세요..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 종합 의견 (모든 권한자) */}
+ <FormField
+ control={form.control}
+ name="consolidatedComment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>종합 의견</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="종합 의견을 입력하세요..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+
+ {/* 담당자 변경 (관리자만) */}
+ {canEditAdminFields() && departments.length > 0 && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">담당자 변경</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 각 부서별 담당자를 변경할 수 있습니다.
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {[
+ { key: "orderReviewerEmail", label: "주문 부서 담당자", current: evaluationTarget.orderReviewerEmail },
+ { key: "procurementReviewerEmail", label: "조달 부서 담당자", current: evaluationTarget.procurementReviewerEmail },
+ { key: "qualityReviewerEmail", label: "품질 부서 담당자", current: evaluationTarget.qualityReviewerEmail },
+ { key: "designReviewerEmail", label: "설계 부서 담당자", current: evaluationTarget.designReviewerEmail },
+ { key: "csReviewerEmail", label: "CS 부서 담당자", current: evaluationTarget.csReviewerEmail },
+ ].map(({ key, label, current }) => {
+ const selectedReviewer = reviewers.find(r => r.email === form.watch(key as keyof EditEvaluationTargetFormValues))
+ const filteredReviewers = getFilteredReviewers(reviewerSearches[key] || "")
+
+ return (
+ <FormField
+ key={key}
+ control={form.control}
+ name={key as keyof EditEvaluationTargetFormValues}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ {label}
+ {current && (
+ <span className="text-xs text-muted-foreground ml-2">
+ (현재: {current})
+ </span>
+ )}
+ </FormLabel>
+ <Popover
+ open={reviewerOpens[key] || false}
+ onOpenChange={(open) => setReviewerOpens(prev => ({...prev, [key]: open}))}
+ >
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={reviewerOpens[key]}
+ className="w-full justify-between"
+ >
+ {selectedReviewer ? (
+ <span className="flex items-center gap-2">
+ <span>{selectedReviewer.name}</span>
+ <span className="text-xs text-muted-foreground">
+ ({selectedReviewer.email})
+ </span>
+ </span>
+ ) : field.value ? (
+ field.value
+ ) : (
+ "담당자 선택..."
+ )}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-full p-0" align="start">
+ <Command>
+ <CommandInput
+ placeholder="담당자 검색..."
+ value={reviewerSearches[key] || ""}
+ onValueChange={(value) => setReviewerSearches(prev => ({...prev, [key]: value}))}
+ />
+ <CommandList>
+ <CommandEmpty>
+ {isLoadingReviewers ? (
+ <div className="flex items-center gap-2">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ 로딩 중...
+ </div>
+ ) : (
+ "검색 결과가 없습니다."
+ )}
+ </CommandEmpty>
+ <CommandGroup>
+ <CommandItem
+ value="선택 안함"
+ onSelect={() => {
+ field.onChange("")
+ setReviewerOpens(prev => ({ ...prev, [key]: false }))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ !field.value ? "opacity-100" : "opacity-0"
+ )}
+ />
+ 선택 안함
+ </CommandItem>
+ {filteredReviewers.map((reviewer) => (
+ <CommandItem
+ key={reviewer.id}
+ value={`${reviewer.name} ${reviewer.email}`}
+ onSelect={() => {
+ field.onChange(reviewer.email)
+ setReviewerOpens(prev => ({...prev, [key]: false}))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ reviewer.email === field.value ? "opacity-100" : "opacity-0"
+ )}
+ />
+ <div className="flex items-center gap-2">
+ <span>{reviewer.name}</span>
+ <span className="text-xs text-muted-foreground">
+ ({reviewer.email})
+ </span>
+ </div>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )
+ })}
+ </CardContent>
+ </Card>
+ )}
+ </div>
+ </div>
+
+ {/* 고정된 버튼 영역 */}
+ <div className="flex-shrink-0 flex justify-end gap-3 pt-6 mt-6 border-t bg-background">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => handleOpenChange(false)}
+ disabled={isSubmitting}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isSubmitting}
+ >
+ {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 수정
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file
diff --git a/lib/evaluation-target-list/validation.ts b/lib/evaluation-target-list/validation.ts
index e42f536b..ce5604be 100644
--- a/lib/evaluation-target-list/validation.ts
+++ b/lib/evaluation-target-list/validation.ts
@@ -46,7 +46,7 @@ import {
>;
export type EvaluationTargetStatus = "PENDING" | "CONFIRMED" | "EXCLUDED";
- export type Division = "OCEAN" | "SHIPYARD";
+ export type Division = "PLANT" | "SHIP";
export type MaterialType = "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK";
export type DomesticForeign = "DOMESTIC" | "FOREIGN";
@@ -54,8 +54,8 @@ import {
export const EVALUATION_TARGET_FILTER_OPTIONS = {
DIVISIONS: [
- { value: "OCEAN", label: "해양" },
- { value: "SHIPYARD", label: "조선" },
+ { value: "PLANT", label: "해양" },
+ { value: "SHIP", label: "조선" },
],
STATUSES: [
{ value: "PENDING", label: "검토 중" },
@@ -86,7 +86,7 @@ import {
}
export function validateDivision(division: string): division is Division {
- return ["OCEAN", "SHIPYARD"].includes(division);
+ return ["PLANT", "SHIP"].includes(division);
}
export function validateStatus(status: string): status is EvaluationTargetStatus {
@@ -142,8 +142,8 @@ import {
// 구분별 라벨 반환
export function getDivisionLabel(division: Division): string {
const divisionMap = {
- OCEAN: "해양",
- SHIPYARD: "조선"
+ PLANT: "해양",
+ SHIP: "조선"
};
return divisionMap[division] || division;
}
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/lib/evaluation/service.ts
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx
new file mode 100644
index 00000000..0c207a53
--- /dev/null
+++ b/lib/evaluation/table/evaluation-columns.tsx
@@ -0,0 +1,441 @@
+// ================================================================
+// 1. PERIODIC EVALUATIONS COLUMNS
+// ================================================================
+
+"use client";
+import * as React from "react";
+import { type ColumnDef } from "@tanstack/react-table";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText } from "lucide-react";
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
+import { PeriodicEvaluationView } from "@/db/schema";
+
+
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PeriodicEvaluationView> | null>>;
+}
+
+// 상태별 색상 매핑
+const getStatusBadgeVariant = (status: string) => {
+ switch (status) {
+ case "PENDING_SUBMISSION":
+ return "outline";
+ case "SUBMITTED":
+ return "secondary";
+ case "IN_REVIEW":
+ return "default";
+ case "REVIEW_COMPLETED":
+ return "default";
+ case "FINALIZED":
+ return "default";
+ default:
+ return "outline";
+ }
+};
+
+const getStatusLabel = (status: string) => {
+ const statusMap = {
+ PENDING_SUBMISSION: "제출대기",
+ SUBMITTED: "제출완료",
+ IN_REVIEW: "검토중",
+ REVIEW_COMPLETED: "검토완료",
+ FINALIZED: "최종확정"
+ };
+ return statusMap[status] || status;
+};
+
+// 등급별 색상
+const getGradeBadgeVariant = (grade: string | null) => {
+ if (!grade) return "outline";
+ switch (grade) {
+ case "S":
+ return "default";
+ case "A":
+ return "secondary";
+ case "B":
+ return "outline";
+ case "C":
+ return "destructive";
+ case "D":
+ return "destructive";
+ default:
+ return "outline";
+ }
+};
+
+// 구분 배지
+const getDivisionBadge = (division: string) => {
+ return (
+ <Badge variant={division === "PLANT" ? "default" : "secondary"}>
+ {division === "PLANT" ? "해양" : "조선"}
+ </Badge>
+ );
+};
+
+// 자재구분 배지
+const getMaterialTypeBadge = (materialType: string) => {
+ const typeMap = {
+ EQUIPMENT: "기자재",
+ BULK: "벌크",
+ EQUIPMENT_BULK: "기자재/벌크"
+ };
+ return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>;
+};
+
+// 내외자 배지
+const getDomesticForeignBadge = (domesticForeign: string) => {
+ return (
+ <Badge variant={domesticForeign === "DOMESTIC" ? "default" : "secondary"}>
+ {domesticForeign === "DOMESTIC" ? "내자" : "외자"}
+ </Badge>
+ );
+};
+
+// 진행률 배지
+const getProgressBadge = (completed: number, total: number) => {
+ if (total === 0) return <Badge variant="outline">-</Badge>;
+
+ const percentage = Math.round((completed / total) * 100);
+ const variant = percentage === 100 ? "default" : percentage >= 50 ? "secondary" : "destructive";
+
+ return <Badge variant={variant}>{completed}/{total} ({percentage}%)</Badge>;
+};
+
+export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ColumnDef<PeriodicEvaluationWithRelations>[] {
+ return [
+ // ═══════════════════════════════════════════════════════════════
+ // 선택 및 기본 정보
+ // ═══════════════════════════════════════════════════════════════
+
+ // Checkbox
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
+ onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
+ aria-label="select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(v) => row.toggleSelected(!!v)}
+ aria-label="select row"
+ className="translate-y-0.5"
+ />
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ },
+
+ // ░░░ 평가년도 ░░░
+ {
+ accessorKey: "evaluationTarget.evaluationYear",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />,
+ cell: ({ row }) => <span className="font-medium">{row.original.evaluationTarget?.evaluationYear}</span>,
+ size: 100,
+ },
+
+ // ░░░ 평가기간 ░░░
+ {
+ accessorKey: "evaluationPeriod",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가기간" />,
+ cell: ({ row }) => (
+ <Badge variant="outline">{row.getValue("evaluationPeriod")}</Badge>
+ ),
+ size: 100,
+ },
+
+ // ░░░ 구분 ░░░
+ {
+ accessorKey: "evaluationTarget.division",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />,
+ cell: ({ row }) => getDivisionBadge(row.original.evaluationTarget?.division || ""),
+ size: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 협력업체 정보
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "협력업체 정보",
+ columns: [
+ {
+ accessorKey: "evaluationTarget.vendorCode",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />,
+ cell: ({ row }) => (
+ <span className="font-mono text-sm">{row.original.evaluationTarget?.vendorCode}</span>
+ ),
+ size: 120,
+ },
+
+ {
+ accessorKey: "evaluationTarget.vendorName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
+ cell: ({ row }) => (
+ <div className="truncate max-w-[200px]" title={row.original.evaluationTarget?.vendorName}>
+ {row.original.evaluationTarget?.vendorName}
+ </div>
+ ),
+ size: 200,
+ },
+
+ {
+ accessorKey: "evaluationTarget.domesticForeign",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
+ cell: ({ row }) => getDomesticForeignBadge(row.original.evaluationTarget?.domesticForeign || ""),
+ size: 80,
+ },
+
+ {
+ accessorKey: "evaluationTarget.materialType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
+ cell: ({ row }) => getMaterialTypeBadge(row.original.evaluationTarget?.materialType || ""),
+ size: 120,
+ },
+ ]
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 제출 현황
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "제출 현황",
+ columns: [
+ {
+ accessorKey: "documentsSubmitted",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="문서제출" />,
+ cell: ({ row }) => {
+ const submitted = row.getValue<boolean>("documentsSubmitted");
+ return (
+ <Badge variant={submitted ? "default" : "destructive"}>
+ {submitted ? "제출완료" : "미제출"}
+ </Badge>
+ );
+ },
+ size: 100,
+ },
+
+ {
+ accessorKey: "submissionDate",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="제출일" />,
+ cell: ({ row }) => {
+ const submissionDate = row.getValue<Date>("submissionDate");
+ return submissionDate ? (
+ <span className="text-sm">
+ {new Intl.DateTimeFormat("ko-KR", {
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(submissionDate))}
+ </span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 80,
+ },
+
+ {
+ accessorKey: "submissionDeadline",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="마감일" />,
+ cell: ({ row }) => {
+ const deadline = row.getValue<Date>("submissionDeadline");
+ if (!deadline) return <span className="text-muted-foreground">-</span>;
+
+ const now = new Date();
+ const isOverdue = now > deadline;
+
+ return (
+ <span className={`text-sm ${isOverdue ? "text-red-600" : ""}`}>
+ {new Intl.DateTimeFormat("ko-KR", {
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(deadline))}
+ </span>
+ );
+ },
+ size: 80,
+ },
+ ]
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 평가 점수
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "평가 점수",
+ columns: [
+ {
+ accessorKey: "totalScore",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="총점" />,
+ cell: ({ row }) => {
+ const score = row.getValue<number>("totalScore");
+ return score ? (
+ <span className="font-medium">{score.toFixed(1)}</span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 80,
+ },
+
+ {
+ accessorKey: "evaluationGrade",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등급" />,
+ cell: ({ row }) => {
+ const grade = row.getValue<string>("evaluationGrade");
+ return grade ? (
+ <Badge variant={getGradeBadgeVariant(grade)}>{grade}</Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 60,
+ },
+
+ {
+ accessorKey: "finalScore",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종점수" />,
+ cell: ({ row }) => {
+ const finalScore = row.getValue<number>("finalScore");
+ return finalScore ? (
+ <span className="font-bold text-green-600">{finalScore.toFixed(1)}</span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 90,
+ },
+
+ {
+ accessorKey: "finalGrade",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종등급" />,
+ cell: ({ row }) => {
+ const finalGrade = row.getValue<string>("finalGrade");
+ return finalGrade ? (
+ <Badge variant={getGradeBadgeVariant(finalGrade)} className="bg-green-600">
+ {finalGrade}
+ </Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 90,
+ },
+ ]
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // 진행 현황
+ // ═══════════════════════════════════════════════════════════════
+ {
+ header: "진행 현황",
+ columns: [
+ {
+ accessorKey: "status",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />,
+ cell: ({ row }) => {
+ const status = row.getValue<string>("status");
+ return (
+ <Badge variant={getStatusBadgeVariant(status)}>
+ {getStatusLabel(status)}
+ </Badge>
+ );
+ },
+ size: 100,
+ },
+
+ {
+ id: "reviewProgress",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="리뷰진행" />,
+ cell: ({ row }) => {
+ const stats = row.original.reviewerStats;
+ if (!stats) return <span className="text-muted-foreground">-</span>;
+
+ return getProgressBadge(stats.completedReviewers, stats.totalReviewers);
+ },
+ size: 120,
+ },
+
+ {
+ accessorKey: "reviewCompletedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="검토완료일" />,
+ cell: ({ row }) => {
+ const completedAt = row.getValue<Date>("reviewCompletedAt");
+ return completedAt ? (
+ <span className="text-sm">
+ {new Intl.DateTimeFormat("ko-KR", {
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(completedAt))}
+ </span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+
+ {
+ accessorKey: "finalizedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />,
+ cell: ({ row }) => {
+ const finalizedAt = row.getValue<Date>("finalizedAt");
+ return finalizedAt ? (
+ <span className="text-sm font-medium">
+ {new Intl.DateTimeFormat("ko-KR", {
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(finalizedAt))}
+ </span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 80,
+ },
+ ]
+ },
+
+ // ░░░ Actions ░░░
+ {
+ id: "actions",
+ enableHiding: false,
+ size: 40,
+ minSize: 40,
+ cell: ({ row }) => {
+ return (
+ <div className="flex items-center gap-1">
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setRowAction({ row, type: "view" })}
+ aria-label="상세보기"
+ title="상세보기"
+ >
+ <Eye className="size-4" />
+ </Button>
+
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setRowAction({ row, type: "update" })}
+ aria-label="수정"
+ title="수정"
+ >
+ <Pencil className="size-4" />
+ </Button>
+ </div>
+ );
+ },
+ },
+ ];
+} \ No newline at end of file
diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx
new file mode 100644
index 00000000..7cda4989
--- /dev/null
+++ b/lib/evaluation/table/evaluation-filter-sheet.tsx
@@ -0,0 +1,1031 @@
+// ================================================================
+// 2. PERIODIC EVALUATIONS FILTER SHEET
+// ================================================================
+
+"use client"
+
+import { useEffect, useTransition, useState, useRef } from "react"
+import { useRouter, useParams } from "next/navigation"
+import { z } from "zod"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Search, X } from "lucide-react"
+import { customAlphabet } from "nanoid"
+import { parseAsStringEnum, useQueryState } from "nuqs"
+
+import { Button } from "@/components/ui/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { cn } from "@/lib/utils"
+import { getFiltersStateParser } from "@/lib/parsers"
+
+// nanoid 생성기
+const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6)
+
+// 정기평가 필터 스키마 정의
+const periodicEvaluationFilterSchema = z.object({
+ evaluationYear: z.string().optional(),
+ evaluationPeriod: z.string().optional(),
+ division: z.string().optional(),
+ status: z.string().optional(),
+ domesticForeign: z.string().optional(),
+ materialType: z.string().optional(),
+ vendorCode: z.string().optional(),
+ vendorName: z.string().optional(),
+ documentsSubmitted: z.string().optional(),
+ evaluationGrade: z.string().optional(),
+ finalGrade: z.string().optional(),
+ minTotalScore: z.string().optional(),
+ maxTotalScore: z.string().optional(),
+})
+
+// 옵션 정의
+const evaluationPeriodOptions = [
+ { value: "상반기", label: "상반기" },
+ { value: "하반기", label: "하반기" },
+ { value: "연간", label: "연간" },
+]
+
+const divisionOptions = [
+ { value: "PLANT", label: "해양" },
+ { value: "SHIP", label: "조선" },
+]
+
+const statusOptions = [
+ { value: "PENDING_SUBMISSION", label: "제출대기" },
+ { value: "SUBMITTED", label: "제출완료" },
+ { value: "IN_REVIEW", label: "검토중" },
+ { value: "REVIEW_COMPLETED", label: "검토완료" },
+ { value: "FINALIZED", label: "최종확정" },
+]
+
+const domesticForeignOptions = [
+ { value: "DOMESTIC", label: "내자" },
+ { value: "FOREIGN", label: "외자" },
+]
+
+const materialTypeOptions = [
+ { value: "EQUIPMENT", label: "기자재" },
+ { value: "BULK", label: "벌크" },
+ { value: "EQUIPMENT_BULK", label: "기자재/벌크" },
+]
+
+const documentsSubmittedOptions = [
+ { value: "true", label: "제출완료" },
+ { value: "false", label: "미제출" },
+]
+
+const gradeOptions = [
+ { value: "S", label: "S등급" },
+ { value: "A", label: "A등급" },
+ { value: "B", label: "B등급" },
+ { value: "C", label: "C등급" },
+ { value: "D", label: "D등급" },
+]
+
+type PeriodicEvaluationFilterFormValues = z.infer<typeof periodicEvaluationFilterSchema>
+
+interface PeriodicEvaluationFilterSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSearch?: () => void;
+ isLoading?: boolean;
+}
+
+export function PeriodicEvaluationFilterSheet({
+ isOpen,
+ onClose,
+ onSearch,
+ isLoading = false
+}: PeriodicEvaluationFilterSheetProps) {
+ const router = useRouter()
+ const params = useParams();
+
+ const [isPending, startTransition] = useTransition()
+ const [isInitializing, setIsInitializing] = useState(false)
+ const lastAppliedFilters = useRef<string>("")
+
+ // nuqs로 URL 상태 관리
+ const [filters, setFilters] = useQueryState(
+ "basicFilters",
+ getFiltersStateParser().withDefault([])
+ )
+
+ const [joinOperator, setJoinOperator] = useQueryState(
+ "basicJoinOperator",
+ parseAsStringEnum(["and", "or"]).withDefault("and")
+ )
+
+ // 폼 상태 초기화
+ const form = useForm<PeriodicEvaluationFilterFormValues>({
+ resolver: zodResolver(periodicEvaluationFilterSchema),
+ defaultValues: {
+ evaluationYear: new Date().getFullYear().toString(),
+ evaluationPeriod: "",
+ division: "",
+ status: "",
+ domesticForeign: "",
+ materialType: "",
+ vendorCode: "",
+ vendorName: "",
+ documentsSubmitted: "",
+ evaluationGrade: "",
+ finalGrade: "",
+ minTotalScore: "",
+ maxTotalScore: "",
+ },
+ })
+
+ // URL 필터에서 초기 폼 상태 설정
+ useEffect(() => {
+ const currentFiltersString = JSON.stringify(filters);
+
+ if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) {
+ setIsInitializing(true);
+
+ const formValues = { ...form.getValues() };
+ let formUpdated = false;
+
+ filters.forEach(filter => {
+ if (filter.id in formValues) {
+ // @ts-ignore - 동적 필드 접근
+ formValues[filter.id] = filter.value;
+ formUpdated = true;
+ }
+ });
+
+ if (formUpdated) {
+ form.reset(formValues);
+ lastAppliedFilters.current = currentFiltersString;
+ }
+
+ setIsInitializing(false);
+ }
+ }, [filters, isOpen])
+
+ // 현재 적용된 필터 카운트
+ const getActiveFilterCount = () => {
+ return filters?.length || 0
+ }
+
+ // 폼 제출 핸들러
+ async function onSubmit(data: PeriodicEvaluationFilterFormValues) {
+ if (isInitializing) return;
+
+ startTransition(async () => {
+ try {
+ const newFilters = []
+
+ if (data.evaluationYear?.trim()) {
+ newFilters.push({
+ id: "evaluationYear",
+ value: parseInt(data.evaluationYear.trim()),
+ type: "number",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.evaluationPeriod?.trim()) {
+ newFilters.push({
+ id: "evaluationPeriod",
+ value: data.evaluationPeriod.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.division?.trim()) {
+ newFilters.push({
+ id: "division",
+ value: data.division.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.status?.trim()) {
+ newFilters.push({
+ id: "status",
+ value: data.status.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.domesticForeign?.trim()) {
+ newFilters.push({
+ id: "domesticForeign",
+ value: data.domesticForeign.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.materialType?.trim()) {
+ newFilters.push({
+ id: "materialType",
+ value: data.materialType.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.vendorCode?.trim()) {
+ newFilters.push({
+ id: "vendorCode",
+ value: data.vendorCode.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.vendorName?.trim()) {
+ newFilters.push({
+ id: "vendorName",
+ value: data.vendorName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.documentsSubmitted?.trim()) {
+ newFilters.push({
+ id: "documentsSubmitted",
+ value: data.documentsSubmitted.trim() === "true",
+ type: "boolean",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.evaluationGrade?.trim()) {
+ newFilters.push({
+ id: "evaluationGrade",
+ value: data.evaluationGrade.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.finalGrade?.trim()) {
+ newFilters.push({
+ id: "finalGrade",
+ value: data.finalGrade.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.minTotalScore?.trim()) {
+ newFilters.push({
+ id: "totalScore",
+ value: parseFloat(data.minTotalScore.trim()),
+ type: "number",
+ operator: "gte",
+ rowId: generateId()
+ })
+ }
+
+ if (data.maxTotalScore?.trim()) {
+ newFilters.push({
+ id: "totalScore",
+ value: parseFloat(data.maxTotalScore.trim()),
+ type: "number",
+ operator: "lte",
+ rowId: generateId()
+ })
+ }
+
+ // URL 업데이트
+ const currentUrl = new URL(window.location.href);
+ const params = new URLSearchParams(currentUrl.search);
+
+ params.delete('basicFilters');
+ params.delete('basicJoinOperator');
+ params.delete('page');
+
+ if (newFilters.length > 0) {
+ params.set('basicFilters', JSON.stringify(newFilters));
+ params.set('basicJoinOperator', joinOperator);
+ }
+
+ params.set('page', '1');
+
+ const newUrl = `${currentUrl.pathname}?${params.toString()}`;
+ window.location.href = newUrl;
+
+ lastAppliedFilters.current = JSON.stringify(newFilters);
+
+ if (onSearch) {
+ onSearch();
+ }
+ } catch (error) {
+ console.error("정기평가 필터 적용 오류:", error);
+ }
+ })
+ }
+
+ // 필터 초기화 핸들러
+ async function handleReset() {
+ try {
+ setIsInitializing(true);
+
+ form.reset({
+ evaluationYear: new Date().getFullYear().toString(),
+ evaluationPeriod: "",
+ division: "",
+ status: "",
+ domesticForeign: "",
+ materialType: "",
+ vendorCode: "",
+ vendorName: "",
+ documentsSubmitted: "",
+ evaluationGrade: "",
+ finalGrade: "",
+ minTotalScore: "",
+ maxTotalScore: "",
+ });
+
+ const currentUrl = new URL(window.location.href);
+ const params = new URLSearchParams(currentUrl.search);
+
+ params.delete('basicFilters');
+ params.delete('basicJoinOperator');
+ params.set('page', '1');
+
+ const newUrl = `${currentUrl.pathname}?${params.toString()}`;
+ window.location.href = newUrl;
+
+ lastAppliedFilters.current = "";
+ setIsInitializing(false);
+ } catch (error) {
+ console.error("정기평가 필터 초기화 오류:", error);
+ setIsInitializing(false);
+ }
+ }
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+ <div className="flex flex-col h-full max-h-full bg-[#F5F7FB] px-6 sm:px-8">
+ {/* Filter Panel Header */}
+ <div className="flex items-center justify-between px-6 min-h-[60px] shrink-0">
+ <h3 className="text-lg font-semibold whitespace-nowrap">정기평가 검색 필터</h3>
+ <div className="flex items-center gap-2">
+ {getActiveFilterCount() > 0 && (
+ <Badge variant="secondary" className="px-2 py-1">
+ {getActiveFilterCount()}개 필터 적용됨
+ </Badge>
+ )}
+ </div>
+ </div>
+
+ {/* Join Operator Selection */}
+ <div className="px-6 shrink-0">
+ <label className="text-sm font-medium">조건 결합 방식</label>
+ <Select
+ value={joinOperator}
+ onValueChange={(value: "and" | "or") => setJoinOperator(value)}
+ disabled={isInitializing}
+ >
+ <SelectTrigger className="h-8 w-[180px] mt-2 bg-white">
+ <SelectValue placeholder="조건 결합 방식" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="and">모든 조건 충족 (AND)</SelectItem>
+ <SelectItem value="or">하나라도 충족 (OR)</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col h-full min-h-0">
+ {/* Scrollable content area */}
+ <div className="flex-1 min-h-0 overflow-y-auto px-6 pb-4">
+ <div className="space-y-4 pt-2">
+
+ {/* 평가년도 */}
+ <FormField
+ control={form.control}
+ name="evaluationYear"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가년도</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ type="number"
+ placeholder="평가년도 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("evaluationYear", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가기간 */}
+ <FormField
+ control={form.control}
+ name="evaluationPeriod"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가기간</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="평가기간 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("evaluationPeriod", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {evaluationPeriodOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 구분 */}
+ <FormField
+ control={form.control}
+ name="division"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>구분</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="구분 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("division", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {divisionOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 진행상태 */}
+ <FormField
+ control={form.control}
+ name="status"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>진행상태</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="진행상태 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("status", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {statusOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 내외자 구분 */}
+ <FormField
+ control={form.control}
+ name="domesticForeign"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>내외자 구분</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="내외자 구분 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("domesticForeign", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {domesticForeignOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 자재구분 */}
+ <FormField
+ control={form.control}
+ name="materialType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>자재구분</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="자재구분 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("materialType", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {materialTypeOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 벤더 코드 */}
+ <FormField
+ control={form.control}
+ name="vendorCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>벤더 코드</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="벤더 코드 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("vendorCode", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 벤더명 */}
+ <FormField
+ control={form.control}
+ name="vendorName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>벤더명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="벤더명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("vendorName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 문서제출여부 */}
+ <FormField
+ control={form.control}
+ name="documentsSubmitted"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>문서제출여부</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="문서제출여부 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("documentsSubmitted", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {documentsSubmittedOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가등급 */}
+ <FormField
+ control={form.control}
+ name="evaluationGrade"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가등급</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="평가등급 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("evaluationGrade", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {gradeOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 최종등급 */}
+ <FormField
+ control={form.control}
+ name="finalGrade"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>최종등급</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder="최종등급 선택" />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("finalGrade", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {gradeOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 점수 범위 */}
+ <div className="grid grid-cols-2 gap-2">
+ <FormField
+ control={form.control}
+ name="minTotalScore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>최소점수</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ type="number"
+ step="0.1"
+ placeholder="최소"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("minTotalScore", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="maxTotalScore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>최대점수</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ type="number"
+ step="0.1"
+ placeholder="최대"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("maxTotalScore", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ </div>
+ </div>
+
+ {/* Fixed buttons at bottom */}
+ <div className="p-4 shrink-0">
+ <div className="flex gap-2 justify-end">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleReset}
+ disabled={isPending || getActiveFilterCount() === 0 || isInitializing}
+ className="px-4"
+ >
+ 초기화
+ </Button>
+ <Button
+ type="submit"
+ variant="samsung"
+ disabled={isPending || isLoading || isInitializing}
+ className="px-4"
+ >
+ <Search className="size-4 mr-2" />
+ {isPending || isLoading ? "조회 중..." : "조회"}
+ </Button>
+ </div>
+ </div>
+ </form>
+ </Form>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx
new file mode 100644
index 00000000..16f70592
--- /dev/null
+++ b/lib/evaluation/table/evaluation-table.tsx
@@ -0,0 +1,462 @@
+"use client"
+
+import * as React from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import { Button } from "@/components/ui/button"
+import { PanelLeftClose, PanelLeftOpen } from "lucide-react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Skeleton } from "@/components/ui/skeleton"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { cn } from "@/lib/utils"
+import { useTablePresets } from "@/components/data-table/use-table-presets"
+import { TablePresetManager } from "@/components/data-table/data-table-preset"
+import { useMemo } from "react"
+import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet"
+import { getPeriodicEvaluationsColumns } from "./evaluation-columns"
+import { PeriodicEvaluationView } from "@/db/schema"
+
+interface PeriodicEvaluationsTableProps {
+ promises: Promise<[Awaited<ReturnType<typeof getPeriodicEvaluations>>]>
+ evaluationYear: number
+ className?: string
+}
+
+// 통계 카드 컴포넌트
+function PeriodicEvaluationsStats({ evaluationYear }: { evaluationYear: number }) {
+ const [stats, setStats] = React.useState<any>(null)
+ const [isLoading, setIsLoading] = React.useState(true)
+ const [error, setError] = React.useState<string | null>(null)
+
+ React.useEffect(() => {
+ let isMounted = true
+
+ async function fetchStats() {
+ try {
+ setIsLoading(true)
+ setError(null)
+ // TODO: getPeriodicEvaluationsStats 구현 필요
+ const statsData = {
+ total: 150,
+ pendingSubmission: 25,
+ submitted: 45,
+ inReview: 30,
+ reviewCompleted: 35,
+ finalized: 15,
+ averageScore: 82.5,
+ completionRate: 75
+ }
+
+ if (isMounted) {
+ setStats(statsData)
+ }
+ } catch (err) {
+ if (isMounted) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch stats')
+ console.error('Error fetching periodic evaluations stats:', err)
+ }
+ } finally {
+ if (isMounted) {
+ setIsLoading(false)
+ }
+ }
+ }
+
+ fetchStats()
+
+ return () => {
+ isMounted = false
+ }
+ }, [])
+
+ if (isLoading) {
+ return (
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
+ {Array.from({ length: 4 }).map((_, i) => (
+ <Card key={i}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <Skeleton className="h-4 w-20" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-8 w-16" />
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ )
+ }
+
+ if (error || !stats) {
+ return (
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
+ <Card className="col-span-full">
+ <CardContent className="pt-6">
+ <div className="text-center text-sm text-muted-foreground">
+ {error ? `통계 데이터를 불러올 수 없습니다: ${error}` : "통계 데이터가 없습니다."}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+ }
+
+ const totalEvaluations = stats.total || 0
+ const pendingSubmission = stats.pendingSubmission || 0
+ const inProgress = (stats.submitted || 0) + (stats.inReview || 0) + (stats.reviewCompleted || 0)
+ const finalized = stats.finalized || 0
+ const completionRate = stats.completionRate || 0
+
+ return (
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
+ {/* 총 평가 */}
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">총 평가</CardTitle>
+ <Badge variant="outline">{evaluationYear}년</Badge>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{totalEvaluations.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground mt-1">
+ 평균점수 {stats.averageScore?.toFixed(1) || 0}점
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 제출대기 */}
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">제출대기</CardTitle>
+ <Badge variant="outline">대기</Badge>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-orange-600">{pendingSubmission.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground mt-1">
+ {totalEvaluations > 0 ? Math.round((pendingSubmission / totalEvaluations) * 100) : 0}% of total
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 진행중 */}
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">진행중</CardTitle>
+ <Badge variant="secondary">진행</Badge>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-blue-600">{inProgress.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground mt-1">
+ {totalEvaluations > 0 ? Math.round((inProgress / totalEvaluations) * 100) : 0}% of total
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 완료율 */}
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">완료율</CardTitle>
+ <Badge variant={completionRate >= 80 ? "default" : completionRate >= 60 ? "secondary" : "destructive"}>
+ {completionRate}%
+ </Badge>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-green-600">{finalized.toLocaleString()}</div>
+ <div className="text-xs text-muted-foreground mt-1">
+ 최종확정 완료
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+}
+
+export function PeriodicEvaluationsTable({ promises, evaluationYear, className }: PeriodicEvaluationsTableProps) {
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<PeriodicEvaluationView> | null>(null)
+ const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
+ const router = useRouter()
+ const searchParams = useSearchParams()
+
+ const containerRef = React.useRef<HTMLDivElement>(null)
+ const [containerTop, setContainerTop] = React.useState(0)
+
+ const updateContainerBounds = React.useCallback(() => {
+ if (containerRef.current) {
+ const rect = containerRef.current.getBoundingClientRect()
+ const newTop = rect.top
+
+ setContainerTop(prevTop => {
+ if (Math.abs(prevTop - newTop) > 1) {
+ return newTop
+ }
+ return prevTop
+ })
+ }
+ }, [])
+
+ const throttledUpdateBounds = React.useCallback(() => {
+ let timeoutId: NodeJS.Timeout
+ return () => {
+ clearTimeout(timeoutId)
+ timeoutId = setTimeout(updateContainerBounds, 16)
+ }
+ }, [updateContainerBounds])
+
+ React.useEffect(() => {
+ updateContainerBounds()
+
+ const throttledHandler = throttledUpdateBounds()
+
+ const handleResize = () => {
+ updateContainerBounds()
+ }
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('scroll', throttledHandler)
+
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('scroll', throttledHandler)
+ }
+ }, [updateContainerBounds, throttledUpdateBounds])
+
+ const [promiseData] = React.use(promises)
+ const tableData = promiseData
+
+ const initialSettings = React.useMemo(() => ({
+ page: parseInt(searchParams.get('page') || '1'),
+ perPage: parseInt(searchParams.get('perPage') || '10'),
+ sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }],
+ filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [],
+ joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and",
+ basicFilters: searchParams.get('basicFilters') ?
+ JSON.parse(searchParams.get('basicFilters')!) : [],
+ basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
+ search: searchParams.get('search') || '',
+ columnVisibility: {},
+ columnOrder: [],
+ pinnedColumns: { left: [], right: ["actions"] },
+ groupBy: [],
+ expandedRows: []
+ }), [searchParams])
+
+ const {
+ presets,
+ activePresetId,
+ hasUnsavedChanges,
+ isLoading: presetsLoading,
+ createPreset,
+ applyPreset,
+ updatePreset,
+ deletePreset,
+ setDefaultPreset,
+ renamePreset,
+ updateClientState,
+ getCurrentSettings,
+ } = useTablePresets<PeriodicEvaluationView>('periodic-evaluations-table', initialSettings)
+
+ const columns = React.useMemo(
+ () => getPeriodicEvaluationsColumns({ setRowAction }),
+ [setRowAction]
+ )
+
+ const filterFields: DataTableFilterField<PeriodicEvaluationView>[] = [
+ { id: "evaluationTarget.vendorCode", label: "벤더 코드" },
+ { id: "evaluationTarget.vendorName", label: "벤더명" },
+ { id: "status", label: "진행상태" },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<PeriodicEvaluationView>[] = [
+ { id: "evaluationTarget.evaluationYear", label: "평가년도", type: "number" },
+ { id: "evaluationPeriod", label: "평가기간", type: "text" },
+ {
+ id: "evaluationTarget.division", label: "구분", type: "select", options: [
+ { label: "해양", value: "PLANT" },
+ { label: "조선", value: "SHIP" },
+ ]
+ },
+ { id: "evaluationTarget.vendorCode", label: "벤더 코드", type: "text" },
+ { id: "evaluationTarget.vendorName", label: "벤더명", type: "text" },
+ {
+ id: "status", label: "진행상태", type: "select", options: [
+ { label: "제출대기", value: "PENDING_SUBMISSION" },
+ { label: "제출완료", value: "SUBMITTED" },
+ { label: "검토중", value: "IN_REVIEW" },
+ { label: "검토완료", value: "REVIEW_COMPLETED" },
+ { label: "최종확정", value: "FINALIZED" },
+ ]
+ },
+ {
+ id: "documentsSubmitted", label: "문서제출", type: "select", options: [
+ { label: "제출완료", value: "true" },
+ { label: "미제출", value: "false" },
+ ]
+ },
+ { id: "totalScore", label: "총점", type: "number" },
+ { id: "finalScore", label: "최종점수", type: "number" },
+ { id: "submissionDate", label: "제출일", type: "date" },
+ { id: "reviewCompletedAt", label: "검토완료일", type: "date" },
+ { id: "finalizedAt", label: "최종확정일", type: "date" },
+ ]
+
+ const currentSettings = useMemo(() => {
+ return getCurrentSettings()
+ }, [getCurrentSettings])
+
+ function getColKey<T>(c: ColumnDef<T>): string | undefined {
+ if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string
+ if ("id" in c && c.id) return c.id as string
+ return undefined
+ }
+
+ const initialState = useMemo(() => {
+ return {
+ sorting: initialSettings.sort.filter(s =>
+ columns.some(c => getColKey(c) === s.id)),
+ columnVisibility: currentSettings.columnVisibility,
+ columnPinning: currentSettings.pinnedColumns,
+ }
+ }, [columns, currentSettings, initialSettings.sort])
+
+ const { table } = useDataTable({
+ data: tableData.data,
+ columns,
+ pageCount: tableData.pageCount,
+ rowCount: tableData.total || tableData.data.length,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState,
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ const handleSearch = () => {
+ setIsFilterPanelOpen(false)
+ }
+
+ const getActiveBasicFilterCount = () => {
+ try {
+ const basicFilters = searchParams.get('basicFilters')
+ return basicFilters ? JSON.parse(basicFilters).length : 0
+ } catch (e) {
+ return 0
+ }
+ }
+
+ const FILTER_PANEL_WIDTH = 400;
+
+ return (
+ <>
+ {/* Filter Panel */}
+ <div
+ className={cn(
+ "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
+ isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
+ )}
+ style={{
+ width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
+ top: `${containerTop}px`,
+ height: `calc(100vh - ${containerTop}px)`
+ }}
+ >
+ <div className="h-full">
+ <PeriodicEvaluationFilterSheet
+ isOpen={isFilterPanelOpen}
+ onClose={() => setIsFilterPanelOpen(false)}
+ onSearch={handleSearch}
+ isLoading={false}
+ />
+ </div>
+ </div>
+
+ {/* Main Content Container */}
+ <div
+ ref={containerRef}
+ className={cn("relative w-full overflow-hidden", className)}
+ >
+ <div className="flex w-full h-full">
+ <div
+ className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
+ style={{
+ width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%',
+ marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px'
+ }}
+ >
+ {/* Header Bar */}
+ <div className="flex items-center justify-between p-4 bg-background shrink-0">
+ <div className="flex items-center gap-3">
+ <Button
+ variant="outline"
+ size="sm"
+ type='button'
+ onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
+ className="flex items-center shadow-sm"
+ >
+ {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
+ {getActiveBasicFilterCount() > 0 && (
+ <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
+ {getActiveBasicFilterCount()}
+ </span>
+ )}
+ </Button>
+ </div>
+
+ <div className="text-sm text-muted-foreground">
+ {tableData && (
+ <span>총 {tableData.total || tableData.data.length}건</span>
+ )}
+ </div>
+ </div>
+
+ {/* 통계 카드들 */}
+ <div className="px-4">
+ <PeriodicEvaluationsStats evaluationYear={evaluationYear} />
+ </div>
+
+ {/* Table Content Area */}
+ <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 500px)' }}>
+ <div className="h-full w-full">
+ <DataTable table={table} className="h-full">
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <div className="flex items-center gap-2">
+ <TablePresetManager<PeriodicEvaluationView>
+ presets={presets}
+ activePresetId={activePresetId}
+ currentSettings={currentSettings}
+ hasUnsavedChanges={hasUnsavedChanges}
+ isLoading={presetsLoading}
+ onCreatePreset={createPreset}
+ onUpdatePreset={updatePreset}
+ onDeletePreset={deletePreset}
+ onApplyPreset={applyPreset}
+ onSetDefaultPreset={setDefaultPreset}
+ onRenamePreset={renamePreset}
+ />
+
+ {/* TODO: PeriodicEvaluationsTableToolbarActions 구현 */}
+ </div>
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ {/* TODO: 수정/상세보기 모달 구현 */}
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ )
+} \ No newline at end of file
diff --git a/lib/evaluation/validation.ts b/lib/evaluation/validation.ts
new file mode 100644
index 00000000..9179f585
--- /dev/null
+++ b/lib/evaluation/validation.ts
@@ -0,0 +1,46 @@
+import {
+ createSearchParamsCache,
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum,
+ } from "nuqs/server";
+ import * as z from "zod";
+
+ import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers";
+import { periodicEvaluations } from "@/db/schema";
+
+ // ============= 메인 검색 파라미터 스키마 =============
+
+ export const searchParamsEvaluationsCache = createSearchParamsCache({
+ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
+ page: parseAsInteger.withDefault(1),
+ perPage: parseAsInteger.withDefault(10),
+ sort: getSortingStateParser<typeof periodicEvaluations>().withDefault([
+ { id: "createdAt", desc: true }]),
+
+ // 기본 필터들
+ evaluationYear: parseAsInteger.withDefault(new Date().getFullYear()),
+ division: parseAsString.withDefault(""),
+ status: parseAsString.withDefault(""),
+ domesticForeign: parseAsString.withDefault(""),
+ materialType: parseAsString.withDefault(""),
+ consensusStatus: parseAsString.withDefault(""),
+
+ // 고급 필터
+ filters: getFiltersStateParser().withDefault([]),
+ joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
+
+ // 베이직 필터 (커스텀 필터 패널용)
+ basicFilters: getFiltersStateParser().withDefault([]),
+ basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
+
+ // 검색
+ search: parseAsString.withDefault(""),
+ });
+
+ // ============= 타입 정의 =============
+
+ export type GetEvaluationsSchema = Awaited<
+ ReturnType<typeof searchParamsEvaluationsCache.parse>
+ >;
diff --git a/lib/forms/services.ts b/lib/forms/services.ts
index 021bb767..0558e83f 100644
--- a/lib/forms/services.ts
+++ b/lib/forms/services.ts
@@ -27,6 +27,7 @@ import { getErrorMessage } from "../handle-error";
import { DataTableColumnJSON } from "@/components/form-data/form-data-table-columns";
import { contractItems, contracts, items, projects } from "@/db/schema";
import { getSEDPToken } from "../sedp/sedp-token";
+import { decryptWithServerAction } from "@/components/drm/drmUtils";
export type FormInfo = InferSelectModel<typeof forms>;
@@ -47,9 +48,9 @@ export async function getFormsByContractItemId(
try {
// return unstable_cache(
// async () => {
- console.log(
- `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}`
- );
+ // console.log(
+ // `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}`
+ // );
try {
// 쿼리 생성
@@ -227,10 +228,9 @@ async function getEditableFieldsByTag(
* 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱.
*/
export async function getFormData(formCode: string, contractItemId: number) {
- const cacheKey = `form-data-${formCode}-${contractItemId}`;
- console.log(cacheKey, "getFormData")
try {
+
// 기존 로직으로 projectId, columns, data 가져오기
const contractItemResult = await db
.select({
@@ -285,6 +285,8 @@ export async function getFormData(formCode: string, contractItemId: number) {
const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO'];
columns = columns.filter(col => !excludeKeys.includes(col.key));
+
+
columns.forEach((col) => {
if (!col.displayLabel) {
if (col.uom) {
@@ -295,25 +297,24 @@ export async function getFormData(formCode: string, contractItemId: number) {
}
});
- // status 컬럼 추가
columns.push({
- key: "status",
- label: "status",
- displayLabel: "Status",
- type: "STRING"
- });
+ key:"status",
+ label:"status",
+ displayLabel:"Status",
+ type:"STRING"
+ })
let data: Array<Record<string, any>> = [];
if (entry) {
if (Array.isArray(entry.data)) {
data = entry.data;
- data.sort((a, b) => {
+ data.sort((a,b) => {
const statusA = a.status || '';
const statusB = b.status || '';
- return statusB.localeCompare(statusA);
- });
-
+ return statusB.localeCompare(statusA)
+ })
+
} else {
console.warn("formEntries data was not an array. Using empty array.");
}
@@ -382,7 +383,7 @@ export async function getFormData(formCode: string, contractItemId: number) {
const entry = entryRows[0] ?? null;
let columns = meta.columns as DataTableColumnJSON[];
- const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO'];
+ const excludeKeys = [ 'BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO'];
columns = columns.filter(col => !excludeKeys.includes(col.key));
columns.forEach((col) => {
@@ -693,7 +694,7 @@ export async function updateFormDataInDB(
...oldItem,
...newData,
TAG_NO: oldItem.TAG_NO, // TAG_NO 변경 불가 시 유지
- status: "Imported from EXCEL" // Excel에서 가져온 데이터임을 표시
+ status: "Updated" // Excel에서 가져온 데이터임을 표시
};
const updatedArray = [...dataArray];
@@ -894,7 +895,8 @@ export async function uploadReportTemp(
const savePath = path.join(baseDir, uniqueName);
- const arrayBuffer = await file.arrayBuffer();
+ // const arrayBuffer = await file.arrayBuffer();
+ const arrayBuffer = await decryptWithServerAction(file);
const buffer = Buffer.from(arrayBuffer);
await fs.mkdir(baseDir, { recursive: true });
diff --git a/lib/incoterms/table/delete-incoterms-dialog.tsx b/lib/incoterms/table/delete-incoterms-dialog.tsx
new file mode 100644
index 00000000..8b91033c
--- /dev/null
+++ b/lib/incoterms/table/delete-incoterms-dialog.tsx
@@ -0,0 +1,154 @@
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Trash } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+
+import { deleteIncoterm } from "../service"
+import { incoterms } from "@/db/schema/procurementRFQ"
+
+interface DeleteIncotermsDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ incoterms: Row<typeof incoterms.$inferSelect>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function DeleteIncotermsDialog({
+ incoterms,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: DeleteIncotermsDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ try {
+ // 각 인코텀즈를 순차적으로 삭제
+ for (const incoterm of incoterms) {
+ const result = await deleteIncoterm(incoterm.code)
+ if (!result.success) {
+ toast.error(`인코텀즈 ${incoterm.code} 삭제 실패: ${result.error}`)
+ return
+ }
+ }
+
+ props.onOpenChange?.(false)
+ toast.success("인코텀즈가 성공적으로 삭제되었습니다.")
+ onSuccess?.()
+ } catch (error) {
+ console.error("Delete error:", error)
+ toast.error("인코텀즈 삭제 중 오류가 발생했습니다.")
+ }
+ })
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ delete ({incoterms.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{incoterms.length}</span>
+ 개의 인코텀즈를 서버에서 영구적으로 삭제합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ delete ({incoterms.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{incoterms.length}</span>
+ 개의 인코텀즈를 서버에서 영구적으로 삭제합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ delete
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ No newline at end of file
diff --git a/lib/incoterms/table/incoterms-add-dialog.tsx b/lib/incoterms/table/incoterms-add-dialog.tsx
index ef378e1e..0f7384d6 100644
--- a/lib/incoterms/table/incoterms-add-dialog.tsx
+++ b/lib/incoterms/table/incoterms-add-dialog.tsx
@@ -3,7 +3,7 @@
import * as React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import * as z from "zod";
import { Plus, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -70,7 +70,8 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) {
try {
const result = await createIncoterm(data);
if (result.data) {
- toast.success("인코텀즈가 추가되었습니다.");
+ toast.success("인코텀즈가 성공적으로 추가되었습니다.");
+ form.reset();
setOpen(false);
if (onSuccess) {
onSuccess();
@@ -89,16 +90,17 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) {
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
- <Button size="sm" variant="outline">
+ <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
인코텀즈 추가
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
- <DialogTitle>인코텀즈 추가</DialogTitle>
+ <DialogTitle>새 인코텀즈 추가</DialogTitle>
<DialogDescription>
새로운 인코텀즈를 추가합니다. 필수 정보를 입력해주세요.
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
</DialogDescription>
</DialogHeader>
@@ -153,7 +155,7 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) {
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- {isLoading ? "생성 중..." : "인코텀즈 추가"}
+ {isLoading ? "생성 중..." : "추가"}
</Button>
</DialogFooter>
</DialogContent>
diff --git a/lib/incoterms/table/incoterms-edit-sheet.tsx b/lib/incoterms/table/incoterms-edit-sheet.tsx
index 9cd067c7..1ae6e902 100644
--- a/lib/incoterms/table/incoterms-edit-sheet.tsx
+++ b/lib/incoterms/table/incoterms-edit-sheet.tsx
@@ -5,6 +5,8 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
+import { Loader } from "lucide-react"
+
import { Button } from "@/components/ui/button"
import {
Form,
@@ -16,8 +18,10 @@ import {
} from "@/components/ui/form"
import {
Sheet,
+ SheetClose,
SheetContent,
SheetDescription,
+ SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
@@ -37,7 +41,7 @@ type UpdateIncotermSchema = z.infer<typeof updateIncotermSchema>
interface IncotermsEditSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
- data: typeof incoterms.$inferSelect
+ data: typeof incoterms.$inferSelect | null
onSuccess: () => void
}
@@ -47,12 +51,14 @@ export function IncotermsEditSheet({
data,
onSuccess,
}: IncotermsEditSheetProps) {
+ const [isUpdatePending, startUpdateTransition] = React.useTransition()
+
const form = useForm<UpdateIncotermSchema>({
resolver: zodResolver(updateIncotermSchema),
defaultValues: {
- code: data.code,
- description: data.description,
- isActive: data.isActive,
+ code: data?.code ?? "",
+ description: data?.description ?? "",
+ isActive: data?.isActive ?? true,
},
mode: "onChange"
})
@@ -68,14 +74,19 @@ export function IncotermsEditSheet({
}, [data, form])
async function onSubmit(input: UpdateIncotermSchema) {
- try {
- await updateIncoterm(data.code, input)
- toast.success("수정이 완료되었습니다.")
- onSuccess()
- onOpenChange(false)
- } catch {
- toast.error("수정 중 오류가 발생했습니다.")
- }
+ if (!data) return
+
+ startUpdateTransition(async () => {
+ try {
+ await updateIncoterm(data.code, input)
+ toast.success("인코텀즈가 성공적으로 수정되었습니다.")
+ onSuccess()
+ onOpenChange(false)
+ } catch (error) {
+ console.error("Update error:", error)
+ toast.error("인코텀즈 수정 중 오류가 발생했습니다.")
+ }
+ })
}
return (
@@ -96,7 +107,7 @@ export function IncotermsEditSheet({
<FormItem>
<FormLabel>코드</FormLabel>
<FormControl>
- <Input {...field} disabled />
+ <Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -132,12 +143,25 @@ export function IncotermsEditSheet({
</FormItem>
)}
/>
- <div className="flex justify-end space-x-2">
- <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
- 취소
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ type="submit"
+ disabled={isUpdatePending || !form.formState.isValid}
+ >
+ {isUpdatePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 저장
</Button>
- <Button type="submit">저장</Button>
- </div>
+ </SheetFooter>
</form>
</Form>
</SheetContent>
diff --git a/lib/incoterms/table/incoterms-table-columns.tsx b/lib/incoterms/table/incoterms-table-columns.tsx
index 56a44e8b..91ce4482 100644
--- a/lib/incoterms/table/incoterms-table-columns.tsx
+++ b/lib/incoterms/table/incoterms-table-columns.tsx
@@ -1,76 +1,71 @@
-import type { ColumnDef, Row } from "@tanstack/react-table";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Ellipsis } from "lucide-react";
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Ellipsis } from "lucide-react"
+
+import { formatDateTime } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
+ DropdownMenuShortcut,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { incoterms } from "@/db/schema/procurementRFQ";
-import { toast } from "sonner";
-import { deleteIncoterm } from "../service";
+} from "@/components/ui/dropdown-menu"
-type Incoterm = typeof incoterms.$inferSelect;
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { incoterms } from "@/db/schema/procurementRFQ"
interface GetColumnsProps {
- setRowAction: (action: { type: string; row: Row<Incoterm> }) => void;
- onSuccess: () => void;
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof incoterms.$inferSelect> | null>>
}
-const handleDelete = async (code: string, onSuccess: () => void) => {
- const result = await deleteIncoterm(code);
- if (result.success) {
- toast.success("삭제 완료");
- onSuccess();
- } else {
- toast.error(result.error || "삭제 실패");
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof incoterms.$inferSelect>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<typeof incoterms.$inferSelect> = {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
}
-};
-export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): ColumnDef<Incoterm>[] {
- return [
- {
- id: "code",
- header: () => <div>코드</div>,
- cell: ({ row }) => <div>{row.original.code}</div>,
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "description",
- header: () => <div>설명</div>,
- cell: ({ row }) => <div>{row.original.description}</div>,
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "isActive",
- header: () => <div>상태</div>,
- cell: ({ row }) => (
- <Badge variant={row.original.isActive ? "default" : "secondary"}>
- {row.original.isActive ? "활성" : "비활성"}
- </Badge>
- ),
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "createdAt",
- header: () => <div>생성일</div>,
- cell: ({ row }) => {
- const value = row.original.createdAt;
- const date = value ? new Date(value) : null;
- return date ? date.toLocaleDateString() : "";
- },
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "actions",
- cell: ({ row }) => (
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<typeof incoterms.$inferSelect> = {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -83,20 +78,99 @@ export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): Column
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
- onSelect={() => setRowAction({ type: "edit", row })}
+ onSelect={() => setRowAction({ row, type: "update" })}
>
- 수정
+ Edit
</DropdownMenuItem>
+
<DropdownMenuSeparator />
<DropdownMenuItem
- onSelect={() => handleDelete(row.original.code, onSuccess)}
- className="text-destructive"
+ onSelect={() => setRowAction({ row, type: "delete" })}
>
- 삭제
+ Delete
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
+ )
+ },
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<typeof incoterms.$inferSelect>[] = [
+ {
+ accessorKey: "code",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="코드" />
+ ),
+ meta: {
+ excelHeader: "코드",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("code") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "description",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설명" />
),
+ meta: {
+ excelHeader: "설명",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("description") ?? "",
+ minSize: 80
},
- ];
+ {
+ accessorKey: "isActive",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="상태" />
+ ),
+ meta: {
+ excelHeader: "상태",
+ type: "boolean",
+ },
+ cell: ({ row }) => {
+ const isActive = row.getValue("isActive") as boolean
+ return (
+ <Badge variant={isActive ? "default" : "secondary"}>
+ {isActive ? "활성" : "비활성"}
+ </Badge>
+ )
+ },
+ minSize: 80
+ },
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ meta: {
+ excelHeader: "생성일",
+ type: "date",
+ },
+ cell: ({ row }) => {
+ const dateVal = row.getValue("createdAt") as Date
+ return formatDateTime(dateVal)
+ },
+ minSize: 80
+ }
+ ]
+
+ // ----------------------------------------------------------------
+ // 4) 최종 컬럼 배열: select, dataColumns, actions
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ ...dataColumns,
+ actionsColumn,
+ ]
} \ No newline at end of file
diff --git a/lib/incoterms/table/incoterms-table-toolbar.tsx b/lib/incoterms/table/incoterms-table-toolbar.tsx
index b87982c9..698acf59 100644
--- a/lib/incoterms/table/incoterms-table-toolbar.tsx
+++ b/lib/incoterms/table/incoterms-table-toolbar.tsx
@@ -1,16 +1,53 @@
"use client";
import * as React from "react";
+import { type Table } from "@tanstack/react-table";
+import { Download } from "lucide-react";
+
+import { exportTableToExcel } from "@/lib/export";
+import { Button } from "@/components/ui/button";
+import { DeleteIncotermsDialog } from "./delete-incoterms-dialog";
import { IncotermsAddDialog } from "./incoterms-add-dialog";
+import { incoterms } from "@/db/schema/procurementRFQ";
-interface IncotermsTableToolbarProps {
+interface IncotermsTableToolbarActionsProps {
+ table: Table<typeof incoterms.$inferSelect>;
onSuccess?: () => void;
}
-export function IncotermsTableToolbar({ onSuccess }: IncotermsTableToolbarProps) {
+export function IncotermsTableToolbarActions({ table, onSuccess }: IncotermsTableToolbarActionsProps) {
return (
<div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteIncotermsDialog
+ incoterms={table
+ .getFilteredSelectedRowModel()
+ .rows.map((row) => row.original)}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false);
+ onSuccess?.();
+ }}
+ />
+ ) : null}
+
<IncotermsAddDialog onSuccess={onSuccess} />
+
+ {/** 3) Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "incoterms-list",
+ excludeColumns: ["select", "actions"],
+ })
+ }
+ className="gap-2"
+ >
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
</div>
);
} \ No newline at end of file
diff --git a/lib/incoterms/table/incoterms-table.tsx b/lib/incoterms/table/incoterms-table.tsx
index c5b5bba4..c98de810 100644
--- a/lib/incoterms/table/incoterms-table.tsx
+++ b/lib/incoterms/table/incoterms-table.tsx
@@ -3,13 +3,16 @@ import * as React from "react";
import { useDataTable } from "@/hooks/use-data-table";
import { DataTable } from "@/components/data-table/data-table";
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
+import type {
+ DataTableAdvancedFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { getIncoterms } from "../service";
import { getColumns } from "./incoterms-table-columns";
-import { incoterms } from "@/db/schema/procurementRFQ";
-import { IncotermsTableToolbar } from "./incoterms-table-toolbar";
-import { toast } from "sonner";
+import { DeleteIncotermsDialog } from "./delete-incoterms-dialog";
import { IncotermsEditSheet } from "./incoterms-edit-sheet";
-import { Row } from "@tanstack/react-table";
-import { getIncoterms } from "../service";
+import { IncotermsTableToolbarActions } from "./incoterms-table-toolbar";
+import { incoterms } from "@/db/schema/procurementRFQ";
interface IncotermsTableProps {
promises?: Promise<[{ data: typeof incoterms.$inferSelect[]; pageCount: number }] >;
@@ -17,8 +20,7 @@ interface IncotermsTableProps {
export function IncotermsTable({ promises }: IncotermsTableProps) {
const [rawData, setRawData] = React.useState<{ data: typeof incoterms.$inferSelect[]; pageCount: number }>({ data: [], pageCount: 0 });
- const [isEditSheetOpen, setIsEditSheetOpen] = React.useState(false);
- const [selectedRow, setSelectedRow] = React.useState<typeof incoterms.$inferSelect | null>(null);
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof incoterms.$inferSelect> | null>(null);
React.useEffect(() => {
if (promises) {
@@ -44,7 +46,6 @@ export function IncotermsTable({ promises }: IncotermsTableProps) {
setRawData(result);
} catch (error) {
console.error("Error refreshing data:", error);
- toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
}
})();
}
@@ -67,50 +68,71 @@ export function IncotermsTable({ promises }: IncotermsTableProps) {
setRawData(result);
} catch (error) {
console.error("Error refreshing data:", error);
- toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
}
}, []);
- const handleRowAction = async (action: { type: string; row: Row<typeof incoterms.$inferSelect> }) => {
- if (action.type === "edit") {
- setSelectedRow(action.row.original);
- setIsEditSheetOpen(true);
- }
- };
+ // 컬럼 설정 - 외부 파일에서 가져옴
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
- const columns = React.useMemo(() => getColumns({ setRowAction: handleRowAction, onSuccess: refreshData }), [refreshData]);
+ // 고급 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<typeof incoterms.$inferSelect>[] = [
+ { id: "code", label: "코드", type: "text" },
+ {
+ id: "isActive", label: "상태", type: "select", options: [
+ { label: "활성", value: "true" },
+ { label: "비활성", value: "false" },
+ ]
+ },
+ { id: "description", label: "설명", type: "text" },
+ { id: "createdAt", label: "생성일", type: "date" },
+ ];
const { table } = useDataTable({
- data: rawData.data,
- columns,
- pageCount: rawData.pageCount,
- filterFields: [],
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.code),
- shallow: false,
- clearOnDefault: true,
- });
+ data: rawData.data,
+ columns,
+ pageCount: rawData.pageCount,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.code),
+ shallow: false,
+ clearOnDefault: true,
+ })
return (
<>
<DataTable table={table}>
- <DataTableAdvancedToolbar table={table} filterFields={[]} shallow={false}>
- <IncotermsTableToolbar onSuccess={refreshData} />
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <IncotermsTableToolbarActions table={table} onSuccess={refreshData} />
</DataTableAdvancedToolbar>
</DataTable>
- {isEditSheetOpen && selectedRow && (
- <IncotermsEditSheet
- open={isEditSheetOpen}
- onOpenChange={setIsEditSheetOpen}
- data={selectedRow}
- onSuccess={refreshData}
- />
- )}
+
+ <DeleteIncotermsDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ incoterms={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ refreshData()
+ }}
+ />
+
+ <IncotermsEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ data={rowAction?.row.original ?? null}
+ onSuccess={refreshData}
+ />
</>
);
} \ No newline at end of file
diff --git a/lib/mail/templates/evaluation-review-request.hbs b/lib/mail/templates/evaluation-review-request.hbs
new file mode 100644
index 00000000..022f438b
--- /dev/null
+++ b/lib/mail/templates/evaluation-review-request.hbs
@@ -0,0 +1,162 @@
+<!-- evaluation-review-request.hbs -->
+<!DOCTYPE html>
+<html lang="ko">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>벤더 평가 의견 요청</title>
+ <style>
+ body {
+ font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #f5f5f5;
+ }
+ .container {
+ background-color: #ffffff;
+ border-radius: 8px;
+ padding: 30px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ }
+ .header {
+ text-align: center;
+ border-bottom: 2px solid #e9ecef;
+ padding-bottom: 20px;
+ margin-bottom: 30px;
+ }
+ .header h1 {
+ color: #2563eb;
+ font-size: 24px;
+ margin: 0;
+ }
+ .content {
+ margin-bottom: 30px;
+ }
+ .info-box {
+ background-color: #f8f9fa;
+ border-left: 4px solid #2563eb;
+ padding: 15px;
+ margin: 20px 0;
+ border-radius: 4px;
+ }
+ .target-list {
+ background-color: #f8f9fa;
+ border-radius: 6px;
+ padding: 15px;
+ margin: 15px 0;
+ }
+ .target-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid #e9ecef;
+ }
+ .target-item:last-child {
+ border-bottom: none;
+ }
+ .vendor-code {
+ background-color: #e9ecef;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: bold;
+ }
+ .button {
+ display: inline-block;
+ background-color: #2563eb;
+ color: white;
+ padding: 12px 24px;
+ text-decoration: none;
+ border-radius: 6px;
+ font-weight: bold;
+ text-align: center;
+ margin: 20px auto;
+ }
+ .button:hover {
+ background-color: #1d4ed8;
+ }
+ .message-box {
+ background-color: #fef3c7;
+ border: 1px solid #f59e0b;
+ border-radius: 6px;
+ padding: 15px;
+ margin: 20px 0;
+ }
+ .footer {
+ border-top: 1px solid #e9ecef;
+ padding-top: 20px;
+ margin-top: 30px;
+ text-align: center;
+ color: #6b7280;
+ font-size: 14px;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>🔍 벤더 평가 의견 요청</h1>
+ </div>
+
+ <div class="content">
+ <p>안녕하세요{{#if reviewerName}}, <strong>{{reviewerName}}</strong>님{{/if}}</p>
+
+ <p><strong>{{requesterName}}</strong>님이 벤더 평가에 대한 의견을 요청하셨습니다.</p>
+
+ <div class="info-box">
+ <p><strong>📋 요청 정보</strong></p>
+ <ul style="margin: 10px 0;">
+ <li>요청 일시: {{requestDate}}</li>
+ <li>평가 대상: {{targetCount}}개 벤더</li>
+ </ul>
+ </div>
+
+ {{#if message}}
+ <div class="message-box">
+ <p><strong>💬 요청자 메시지:</strong></p>
+ <p style="margin: 8px 0; white-space: pre-line;">{{message}}</p>
+ </div>
+ {{/if}}
+
+ <div class="target-list">
+ <p><strong>📄 평가 대상 목록:</strong></p>
+ {{#each targets}}
+ <div class="target-item">
+ <div>
+ <span class="vendor-code">{{this.vendorCode}}</span>
+ <span style="margin-left: 8px;">{{this.vendorName}}</span>
+ </div>
+ <div style="font-size: 12px; color: #6b7280;">
+ {{this.materialType}} ({{this.evaluationYear}}년)
+ </div>
+ </div>
+ {{/each}}
+ </div>
+
+ <div style="text-align: center;">
+ <a href="{{reviewUrl}}" class="button">
+ 📝 평가 의견 작성하기
+ </a>
+ </div>
+
+ <div class="info-box">
+ <p><strong>💡 참고사항:</strong></p>
+ <ul style="margin: 10px 0;">
+ <li>평가 시스템에 로그인하여 각 벤더에 대한 의견을 입력해주세요.</li>
+ <li>평가 여부(여/부)와 함께 종합 의견도 작성 가능합니다.</li>
+ <li>궁금한 사항이 있으시면 요청자에게 직접 문의해주세요.</li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="footer">
+ <p>이 메일은 벤더 평가 시스템에서 자동으로 발송되었습니다.</p>
+ <p>문의사항이 있으시면 시스템 관리자에게 연락해주세요.</p>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/lib/payment-terms/table/delete-payment-terms-dialog.tsx b/lib/payment-terms/table/delete-payment-terms-dialog.tsx
new file mode 100644
index 00000000..3e955fce
--- /dev/null
+++ b/lib/payment-terms/table/delete-payment-terms-dialog.tsx
@@ -0,0 +1,154 @@
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Trash } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+
+import { deletePaymentTerm } from "../service"
+import { paymentTerms } from "@/db/schema/procurementRFQ"
+
+interface DeletePaymentTermsDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ paymentTerms: Row<typeof paymentTerms.$inferSelect>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function DeletePaymentTermsDialog({
+ paymentTerms,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: DeletePaymentTermsDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ try {
+ // 각 결제 조건을 순차적으로 삭제
+ for (const paymentTerm of paymentTerms) {
+ const result = await deletePaymentTerm(paymentTerm.code)
+ if (!result.success) {
+ toast.error(`결제 조건 ${paymentTerm.code} 삭제 실패: ${result.error}`)
+ return
+ }
+ }
+
+ props.onOpenChange?.(false)
+ toast.success("결제 조건이 성공적으로 삭제되었습니다.")
+ onSuccess?.()
+ } catch (error) {
+ console.error("Delete error:", error)
+ toast.error("결제 조건 삭제 중 오류가 발생했습니다.")
+ }
+ })
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ delete ({paymentTerms.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{paymentTerms.length}</span>
+ 개의 결제 조건을 서버에서 영구적으로 삭제합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ delete ({paymentTerms.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{paymentTerms.length}</span>
+ 개의 결제 조건을 서버에서 영구적으로 삭제합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ 삭제
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ No newline at end of file
diff --git a/lib/payment-terms/table/payment-terms-add-dialog.tsx b/lib/payment-terms/table/payment-terms-add-dialog.tsx
index 9aa21485..49819f87 100644
--- a/lib/payment-terms/table/payment-terms-add-dialog.tsx
+++ b/lib/payment-terms/table/payment-terms-add-dialog.tsx
@@ -70,7 +70,8 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps)
try {
const result = await createPaymentTerm(data);
if (result.data) {
- toast.success("결제 조건이 추가되었습니다.");
+ toast.success("결제 조건이 성공적으로 추가되었습니다.");
+ form.reset();
setOpen(false);
if (onSuccess) {
onSuccess();
@@ -89,16 +90,17 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps)
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
- <Button size="sm" variant="outline">
+ <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
결제 조건 추가
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
- <DialogTitle>결제 조건 추가</DialogTitle>
+ <DialogTitle>새 결제 조건 추가</DialogTitle>
<DialogDescription>
새로운 결제 조건을 추가합니다. 필수 정보를 입력해주세요.
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
</DialogDescription>
</DialogHeader>
@@ -153,7 +155,7 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps)
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- {isLoading ? "생성 중..." : "결제 조건 추가"}
+ {isLoading ? "생성 중..." : "추가"}
</Button>
</DialogFooter>
</DialogContent>
diff --git a/lib/payment-terms/table/payment-terms-edit-sheet.tsx b/lib/payment-terms/table/payment-terms-edit-sheet.tsx
index b0d105bc..48d79c21 100644
--- a/lib/payment-terms/table/payment-terms-edit-sheet.tsx
+++ b/lib/payment-terms/table/payment-terms-edit-sheet.tsx
@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
+import { Loader } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
@@ -17,8 +18,10 @@ import {
} from "@/components/ui/form"
import {
Sheet,
+ SheetClose,
SheetContent,
SheetDescription,
+ SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
@@ -38,7 +41,7 @@ type UpdatePaymentTermSchema = z.infer<typeof updatePaymentTermSchema>
interface PaymentTermsEditSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
- data: typeof paymentTerms.$inferSelect
+ data: typeof paymentTerms.$inferSelect | null
onSuccess: () => void
}
@@ -48,12 +51,14 @@ export function PaymentTermsEditSheet({
data,
onSuccess,
}: PaymentTermsEditSheetProps) {
+ const [isUpdatePending, startUpdateTransition] = React.useTransition()
+
const form = useForm<UpdatePaymentTermSchema>({
resolver: zodResolver(updatePaymentTermSchema),
defaultValues: {
- code: data.code,
- description: data.description,
- isActive: data.isActive,
+ code: data?.code ?? "",
+ description: data?.description ?? "",
+ isActive: data?.isActive ?? true,
},
mode: "onChange"
})
@@ -69,14 +74,19 @@ export function PaymentTermsEditSheet({
}, [data, form])
async function onSubmit(input: UpdatePaymentTermSchema) {
- try {
- await updatePaymentTerm(data.code, input)
- toast.success("수정이 완료되었습니다.")
- onSuccess()
- onOpenChange(false)
- } catch {
- toast.error("수정 중 오류가 발생했습니다.")
- }
+ if (!data) return
+
+ startUpdateTransition(async () => {
+ try {
+ await updatePaymentTerm(data.code, input)
+ toast.success("결제 조건이 성공적으로 수정되었습니다.")
+ onSuccess()
+ onOpenChange(false)
+ } catch (error) {
+ console.error("Update error:", error)
+ toast.error("결제 조건 수정 중 오류가 발생했습니다.")
+ }
+ })
}
return (
@@ -97,7 +107,7 @@ export function PaymentTermsEditSheet({
<FormItem>
<FormLabel>코드</FormLabel>
<FormControl>
- <Input {...field} disabled />
+ <Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -133,12 +143,25 @@ export function PaymentTermsEditSheet({
</FormItem>
)}
/>
- <div className="flex justify-end space-x-2">
- <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
- 취소
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ type="submit"
+ disabled={isUpdatePending || !form.formState.isValid}
+ >
+ {isUpdatePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 저장
</Button>
- <Button type="submit">저장</Button>
- </div>
+ </SheetFooter>
</form>
</Form>
</SheetContent>
diff --git a/lib/payment-terms/table/payment-terms-table-columns.tsx b/lib/payment-terms/table/payment-terms-table-columns.tsx
index 208723f7..08d30482 100644
--- a/lib/payment-terms/table/payment-terms-table-columns.tsx
+++ b/lib/payment-terms/table/payment-terms-table-columns.tsx
@@ -1,76 +1,71 @@
-import { type ColumnDef, type Row } from "@tanstack/react-table";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Ellipsis } from "lucide-react";
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Ellipsis } from "lucide-react"
+
+import { formatDateTime } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
+ DropdownMenuShortcut,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { paymentTerms } from "@/db/schema/procurementRFQ";
-import { toast } from "sonner";
-import { deletePaymentTerm } from "../service";
+} from "@/components/ui/dropdown-menu"
-type PaymentTerm = typeof paymentTerms.$inferSelect;
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { paymentTerms } from "@/db/schema/procurementRFQ"
interface GetColumnsProps {
- setRowAction: (action: { type: string; row: Row<PaymentTerm> }) => void;
- onSuccess: () => void;
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof paymentTerms.$inferSelect> | null>>
}
-const handleDelete = async (code: string, onSuccess: () => void) => {
- const result = await deletePaymentTerm(code);
- if (result.success) {
- toast.success("삭제 완료");
- onSuccess();
- } else {
- toast.error(result.error || "삭제 실패");
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof paymentTerms.$inferSelect>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<typeof paymentTerms.$inferSelect> = {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
}
-};
-export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): ColumnDef<PaymentTerm>[] {
- return [
- {
- id: "code",
- header: () => <div>코드</div>,
- cell: ({ row }) => <div>{row.original.code}</div>,
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "description",
- header: () => <div>설명</div>,
- cell: ({ row }) => <div>{row.original.description}</div>,
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "isActive",
- header: () => <div>상태</div>,
- cell: ({ row }) => (
- <Badge variant={row.original.isActive ? "default" : "secondary"}>
- {row.original.isActive ? "활성" : "비활성"}
- </Badge>
- ),
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "createdAt",
- header: () => <div>생성일</div>,
- cell: ({ row }) => {
- const value = row.original.createdAt;
- const date = value ? new Date(value) : null;
- return date ? date.toLocaleDateString() : "";
- },
- enableSorting: true,
- enableHiding: false,
- },
- {
- id: "actions",
- cell: ({ row }) => (
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<typeof paymentTerms.$inferSelect> = {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -83,20 +78,99 @@ export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): Column
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
- onSelect={() => setRowAction({ type: "edit", row })}
+ onSelect={() => setRowAction({ row, type: "update" })}
>
- 수정
+ Edit
</DropdownMenuItem>
+
<DropdownMenuSeparator />
<DropdownMenuItem
- onSelect={() => handleDelete(row.original.code, onSuccess)}
- className="text-destructive"
+ onSelect={() => setRowAction({ row, type: "delete" })}
>
- 삭제
+ Delete
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
+ )
+ },
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<typeof paymentTerms.$inferSelect>[] = [
+ {
+ accessorKey: "code",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="코드" />
+ ),
+ meta: {
+ excelHeader: "코드",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("code") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "description",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설명" />
),
+ meta: {
+ excelHeader: "설명",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("description") ?? "",
+ minSize: 80
},
- ];
+ {
+ accessorKey: "isActive",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="상태" />
+ ),
+ meta: {
+ excelHeader: "상태",
+ type: "boolean",
+ },
+ cell: ({ row }) => {
+ const isActive = row.getValue("isActive") as boolean
+ return (
+ <Badge variant={isActive ? "default" : "secondary"}>
+ {isActive ? "활성" : "비활성"}
+ </Badge>
+ )
+ },
+ minSize: 80
+ },
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ meta: {
+ excelHeader: "생성일",
+ type: "date",
+ },
+ cell: ({ row }) => {
+ const dateVal = row.getValue("createdAt") as Date
+ return formatDateTime(dateVal)
+ },
+ minSize: 80
+ }
+ ]
+
+ // ----------------------------------------------------------------
+ // 4) 최종 컬럼 배열: select, dataColumns, actions
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ ...dataColumns,
+ actionsColumn,
+ ]
} \ No newline at end of file
diff --git a/lib/payment-terms/table/payment-terms-table-toolbar.tsx b/lib/payment-terms/table/payment-terms-table-toolbar.tsx
index 2466a9e4..51ac9b93 100644
--- a/lib/payment-terms/table/payment-terms-table-toolbar.tsx
+++ b/lib/payment-terms/table/payment-terms-table-toolbar.tsx
@@ -1,16 +1,53 @@
"use client";
import * as React from "react";
+import { type Table } from "@tanstack/react-table";
+import { Download } from "lucide-react";
+
+import { exportTableToExcel } from "@/lib/export";
+import { Button } from "@/components/ui/button";
+import { DeletePaymentTermsDialog } from "./delete-payment-terms-dialog";
import { PaymentTermsAddDialog } from "./payment-terms-add-dialog";
+import { paymentTerms } from "@/db/schema/procurementRFQ";
-interface PaymentTermsTableToolbarProps {
+interface PaymentTermsTableToolbarActionsProps {
+ table: Table<typeof paymentTerms.$inferSelect>;
onSuccess?: () => void;
}
-export function PaymentTermsTableToolbar({ onSuccess }: PaymentTermsTableToolbarProps) {
+export function PaymentTermsTableToolbarActions({ table, onSuccess }: PaymentTermsTableToolbarActionsProps) {
return (
<div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeletePaymentTermsDialog
+ paymentTerms={table
+ .getFilteredSelectedRowModel()
+ .rows.map((row) => row.original)}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false);
+ onSuccess?.();
+ }}
+ />
+ ) : null}
+
<PaymentTermsAddDialog onSuccess={onSuccess} />
+
+ {/** 3) Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "payment-terms-list",
+ excludeColumns: ["select", "actions"],
+ })
+ }
+ className="gap-2"
+ >
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
</div>
);
} \ No newline at end of file
diff --git a/lib/payment-terms/table/payment-terms-table.tsx b/lib/payment-terms/table/payment-terms-table.tsx
index 589acb52..ddf270ce 100644
--- a/lib/payment-terms/table/payment-terms-table.tsx
+++ b/lib/payment-terms/table/payment-terms-table.tsx
@@ -3,13 +3,16 @@ import * as React from "react";
import { useDataTable } from "@/hooks/use-data-table";
import { DataTable } from "@/components/data-table/data-table";
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
+import type {
+ DataTableAdvancedFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { getPaymentTerms } from "../service";
import { getColumns } from "./payment-terms-table-columns";
-import { paymentTerms } from "@/db/schema/procurementRFQ";
-import { PaymentTermsTableToolbar } from "./payment-terms-table-toolbar";
-import { toast } from "sonner";
+import { DeletePaymentTermsDialog } from "./delete-payment-terms-dialog";
import { PaymentTermsEditSheet } from "./payment-terms-edit-sheet";
-import { Row } from "@tanstack/react-table";
-import { getPaymentTerms } from "../service";
+import { PaymentTermsTableToolbarActions } from "./payment-terms-table-toolbar";
+import { paymentTerms } from "@/db/schema/procurementRFQ";
import { GetPaymentTermsSchema } from "../validations";
interface PaymentTermsTableProps {
@@ -18,8 +21,7 @@ interface PaymentTermsTableProps {
export function PaymentTermsTable({ promises }: PaymentTermsTableProps) {
const [rawData, setRawData] = React.useState<{ data: typeof paymentTerms.$inferSelect[]; pageCount: number }>({ data: [], pageCount: 0 });
- const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false);
- const [selectedRow, setSelectedRow] = React.useState<typeof paymentTerms.$inferSelect | null>(null);
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof paymentTerms.$inferSelect> | null>(null);
React.useEffect(() => {
if (promises) {
@@ -45,7 +47,6 @@ export function PaymentTermsTable({ promises }: PaymentTermsTableProps) {
setRawData(result);
} catch (error) {
console.error("Error refreshing data:", error);
- toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
}
})();
}
@@ -78,50 +79,71 @@ export function PaymentTermsTable({ promises }: PaymentTermsTableProps) {
setRawData(result);
} catch (error) {
console.error("Error refreshing data:", error);
- toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
}
}, [fetchPaymentTerms]);
- const handleRowAction = async (action: { type: string; row: Row<typeof paymentTerms.$inferSelect> }) => {
- if (action.type === "edit") {
- setSelectedRow(action.row.original);
- setIsEditDialogOpen(true);
- }
- };
+ // 컬럼 설정 - 외부 파일에서 가져옴
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
- const columns = React.useMemo(() => getColumns({ setRowAction: handleRowAction, onSuccess: refreshData }), [refreshData]);
+ // 고급 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<typeof paymentTerms.$inferSelect>[] = [
+ { id: "code", label: "코드", type: "text" },
+ {
+ id: "isActive", label: "상태", type: "select", options: [
+ { label: "활성", value: "true" },
+ { label: "비활성", value: "false" },
+ ]
+ },
+ { id: "description", label: "설명", type: "text" },
+ { id: "createdAt", label: "생성일", type: "date" },
+ ];
const { table } = useDataTable({
- data: rawData.data,
- columns,
- pageCount: rawData.pageCount,
- filterFields: [],
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.code),
- shallow: false,
- clearOnDefault: true,
- });
+ data: rawData.data,
+ columns,
+ pageCount: rawData.pageCount,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.code),
+ shallow: false,
+ clearOnDefault: true,
+ })
return (
<>
<DataTable table={table}>
- <DataTableAdvancedToolbar table={table} filterFields={[]} shallow={false}>
- <PaymentTermsTableToolbar onSuccess={refreshData} />
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <PaymentTermsTableToolbarActions table={table} onSuccess={refreshData} />
</DataTableAdvancedToolbar>
</DataTable>
- {isEditDialogOpen && selectedRow && (
- <PaymentTermsEditSheet
- open={isEditDialogOpen}
- onOpenChange={setIsEditDialogOpen}
- data={selectedRow}
- onSuccess={refreshData}
- />
- )}
+
+ <DeletePaymentTermsDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ paymentTerms={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ refreshData()
+ }}
+ />
+
+ <PaymentTermsEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ data={rowAction?.row.original ?? null}
+ onSuccess={refreshData}
+ />
</>
);
} \ No newline at end of file
diff --git a/lib/project-gtc/service.ts b/lib/project-gtc/service.ts
new file mode 100644
index 00000000..c65d9364
--- /dev/null
+++ b/lib/project-gtc/service.ts
@@ -0,0 +1,389 @@
+"use server";
+
+import { revalidateTag } from "next/cache";
+import db from "@/db/db";
+import { unstable_cache } from "@/lib/unstable-cache";
+import { asc, desc, ilike, or, eq, count, and, ne, sql } from "drizzle-orm";
+import {
+ projectGtcFiles,
+ projectGtcView,
+ type ProjectGtcFile,
+ projects,
+} from "@/db/schema";
+import { promises as fs } from "fs";
+import path from "path";
+import crypto from "crypto";
+import { revalidatePath } from 'next/cache';
+
+// Project GTC 목록 조회
+export async function getProjectGtcList(
+ input: {
+ page: number;
+ perPage: number;
+ search?: string;
+ sort: Array<{ id: string; desc: boolean }>;
+ filters?: Record<string, unknown>;
+ }
+) {
+ return unstable_cache(
+ async () => {
+ try {
+ const offset = (input.page - 1) * input.perPage;
+
+ const { data, total } = await db.transaction(async (tx) => {
+ let whereCondition = undefined;
+
+ // GTC 파일이 있는 프로젝트만 필터링
+ const gtcFileCondition = sql`${projectGtcView.gtcFileId} IS NOT NULL`;
+
+ if (input.search) {
+ const s = `%${input.search}%`;
+ const searchCondition = or(
+ ilike(projectGtcView.code, s),
+ ilike(projectGtcView.name, s),
+ ilike(projectGtcView.type, s),
+ ilike(projectGtcView.originalFileName, s)
+ );
+ whereCondition = and(gtcFileCondition, searchCondition);
+ } else {
+ whereCondition = gtcFileCondition;
+ }
+
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) =>
+ item.desc
+ ? desc(
+ projectGtcView[
+ item.id as keyof typeof projectGtcView
+ ] as never
+ )
+ : asc(
+ projectGtcView[
+ item.id as keyof typeof projectGtcView
+ ] as never
+ )
+ )
+ : [desc(projectGtcView.projectCreatedAt)];
+
+ const dataResult = await tx
+ .select()
+ .from(projectGtcView)
+ .where(whereCondition)
+ .orderBy(...orderBy)
+ .limit(input.perPage)
+ .offset(offset);
+
+ const totalCount = await tx
+ .select({ count: count() })
+ .from(projectGtcView)
+ .where(whereCondition);
+
+ return {
+ data: dataResult,
+ total: totalCount[0]?.count || 0,
+ };
+ });
+
+ const pageCount = Math.ceil(total / input.perPage);
+
+ return {
+ data,
+ pageCount,
+ };
+ } catch (error) {
+ console.error("getProjectGtcList 에러:", error);
+ throw new Error("Project GTC 목록을 가져오는 중 오류가 발생했습니다.");
+ }
+ },
+ [`project-gtc-list-${JSON.stringify(input)}`],
+ {
+ tags: ["project-gtc"],
+ revalidate: false,
+ }
+ )();
+}
+
+// Project GTC 파일 업로드
+export async function uploadProjectGtcFile(
+ projectId: number,
+ file: File
+): Promise<{ success: boolean; data?: ProjectGtcFile; error?: string }> {
+ try {
+ // 유효성 검사
+ if (!projectId) {
+ return { success: false, error: "프로젝트 ID는 필수입니다." };
+ }
+
+ if (!file) {
+ return { success: false, error: "파일은 필수입니다." };
+ }
+
+ // 허용된 파일 타입 검사
+ const allowedTypes = [
+ 'application/pdf',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'text/plain'
+ ];
+
+ if (!allowedTypes.includes(file.type)) {
+ return { success: false, error: "PDF, Word, 또는 텍스트 파일만 업로드 가능합니다." };
+ }
+
+ // 원본 파일 이름과 확장자 분리
+ const originalFileName = file.name;
+ const fileExtension = path.extname(originalFileName);
+ const fileNameWithoutExt = path.basename(originalFileName, fileExtension);
+
+ // 해시된 파일 이름 생성
+ const timestamp = Date.now();
+ const randomHash = crypto.createHash('md5')
+ .update(`${fileNameWithoutExt}-${timestamp}-${Math.random()}`)
+ .digest('hex')
+ .substring(0, 8);
+
+ const hashedFileName = `${timestamp}-${randomHash}${fileExtension}`;
+
+ // 저장 디렉토리 설정
+ const uploadDir = path.join(process.cwd(), "public", "project-gtc");
+
+ // 디렉토리가 없으면 생성
+ try {
+ await fs.mkdir(uploadDir, { recursive: true });
+ } catch (err) {
+ console.log("Directory already exists or creation failed:", err);
+ }
+
+ // 파일 경로 설정
+ const filePath = path.join(uploadDir, hashedFileName);
+ const publicFilePath = `/project-gtc/${hashedFileName}`;
+
+ // 파일을 ArrayBuffer로 변환
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+
+ // 파일 저장
+ await fs.writeFile(filePath, buffer);
+
+ // 기존 파일이 있으면 삭제
+ const existingFile = await db.query.projectGtcFiles.findFirst({
+ where: eq(projectGtcFiles.projectId, projectId)
+ });
+
+ if (existingFile) {
+ // 기존 파일 삭제
+ try {
+ const filePath = path.join(process.cwd(), "public", existingFile.filePath);
+ await fs.unlink(filePath);
+ } catch {
+ console.error("파일 삭제 실패");
+ }
+
+ // DB에서 기존 파일 정보 삭제
+ await db.delete(projectGtcFiles)
+ .where(eq(projectGtcFiles.id, existingFile.id));
+ }
+
+ // DB에 새 파일 정보 저장
+ const newFile = await db.insert(projectGtcFiles).values({
+ projectId,
+ fileName: hashedFileName,
+ filePath: publicFilePath,
+ originalFileName,
+ fileSize: file.size,
+ mimeType: file.type,
+ }).returning();
+
+ revalidateTag("project-gtc");
+ revalidatePath("/evcp/project-gtc");
+
+ return { success: true, data: newFile[0] };
+
+ } catch (error) {
+ console.error("Project GTC 파일 업로드 에러:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다."
+ };
+ }
+}
+
+// Project GTC 파일 삭제
+export async function deleteProjectGtcFile(
+ projectId: number
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ return await db.transaction(async (tx) => {
+ const existingFile = await tx.query.projectGtcFiles.findFirst({
+ where: eq(projectGtcFiles.projectId, projectId)
+ });
+
+ if (!existingFile) {
+ return { success: false, error: "삭제할 파일이 없습니다." };
+ }
+
+ // 파일 시스템에서 파일 삭제
+ try {
+ const filePath = path.join(process.cwd(), "public", existingFile.filePath);
+ await fs.unlink(filePath);
+ } catch (error) {
+ console.error("파일 시스템에서 파일 삭제 실패:", error);
+ throw new Error("파일 시스템에서 파일 삭제에 실패했습니다.");
+ }
+
+ // DB에서 파일 정보 삭제
+ await tx.delete(projectGtcFiles)
+ .where(eq(projectGtcFiles.id, existingFile.id));
+
+ return { success: true };
+ });
+
+ } catch (error) {
+ console.error("Project GTC 파일 삭제 에러:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "파일 삭제 중 오류가 발생했습니다."
+ };
+ } finally {
+ // 트랜잭션 성공/실패와 관계없이 캐시 무효화
+ revalidateTag("project-gtc");
+ revalidatePath("/evcp/project-gtc");
+ }
+}
+
+// 프로젝트별 GTC 파일 정보 조회
+export async function getProjectGtcFile(projectId: number): Promise<ProjectGtcFile | null> {
+ try {
+ const file = await db.query.projectGtcFiles.findFirst({
+ where: eq(projectGtcFiles.projectId, projectId)
+ });
+
+ return file || null;
+ } catch (error) {
+ console.error("Project GTC 파일 조회 에러:", error);
+ return null;
+ }
+}
+
+// 프로젝트 생성 서버 액션
+export async function createProject(
+ input: {
+ code: string;
+ name: string;
+ type: string;
+ }
+): Promise<{ success: boolean; data?: typeof projects.$inferSelect; error?: string }> {
+ try {
+ // 유효성 검사
+ if (!input.code?.trim()) {
+ return { success: false, error: "프로젝트 코드는 필수입니다." };
+ }
+
+ if (!input.name?.trim()) {
+ return { success: false, error: "프로젝트명은 필수입니다." };
+ }
+
+ if (!input.type?.trim()) {
+ return { success: false, error: "프로젝트 타입은 필수입니다." };
+ }
+
+ // 프로젝트 코드 중복 검사
+ const existingProject = await db.query.projects.findFirst({
+ where: eq(projects.code, input.code.trim())
+ });
+
+ if (existingProject) {
+ return { success: false, error: "이미 존재하는 프로젝트 코드입니다." };
+ }
+
+ // 프로젝트 생성
+ const newProject = await db.insert(projects).values({
+ code: input.code.trim(),
+ name: input.name.trim(),
+ type: input.type.trim(),
+ }).returning();
+
+ revalidateTag("project-gtc");
+ revalidatePath("/evcp/project-gtc");
+
+ return { success: true, data: newProject[0] };
+
+ } catch (error) {
+ console.error("프로젝트 생성 에러:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "프로젝트 생성 중 오류가 발생했습니다."
+ };
+ }
+}
+
+// 프로젝트 정보 수정 서버 액션
+export async function updateProject(
+ input: {
+ id: number;
+ code: string;
+ name: string;
+ type: string;
+ }
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ if (!input.id) {
+ return { success: false, error: "프로젝트 ID는 필수입니다." };
+ }
+ if (!input.code?.trim()) {
+ return { success: false, error: "프로젝트 코드는 필수입니다." };
+ }
+ if (!input.name?.trim()) {
+ return { success: false, error: "프로젝트명은 필수입니다." };
+ }
+ if (!input.type?.trim()) {
+ return { success: false, error: "프로젝트 타입은 필수입니다." };
+ }
+
+ // 프로젝트 코드 중복 검사 (본인 제외)
+ const existingProject = await db.query.projects.findFirst({
+ where: and(
+ eq(projects.code, input.code.trim()),
+ ne(projects.id, input.id)
+ )
+ });
+ if (existingProject) {
+ return { success: false, error: "이미 존재하는 프로젝트 코드입니다." };
+ }
+
+ // 업데이트
+ await db.update(projects)
+ .set({
+ code: input.code.trim(),
+ name: input.name.trim(),
+ type: input.type.trim(),
+ })
+ .where(eq(projects.id, input.id));
+
+ revalidateTag("project-gtc");
+ revalidatePath("/evcp/project-gtc");
+
+ return { success: true };
+ } catch (error) {
+ console.error("프로젝트 수정 에러:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "프로젝트 수정 중 오류가 발생했습니다."
+ };
+ }
+}
+
+// 이미 GTC 파일이 등록된 프로젝트 ID 목록 조회
+export async function getProjectsWithGtcFiles(): Promise<number[]> {
+ try {
+ const result = await db
+ .select({ projectId: projectGtcFiles.projectId })
+ .from(projectGtcFiles);
+
+ return result.map(row => row.projectId);
+ } catch (error) {
+ console.error("GTC 파일이 등록된 프로젝트 조회 에러:", error);
+ return [];
+ }
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/add-project-dialog.tsx b/lib/project-gtc/table/add-project-dialog.tsx
new file mode 100644
index 00000000..616ab950
--- /dev/null
+++ b/lib/project-gtc/table/add-project-dialog.tsx
@@ -0,0 +1,296 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import { toast } from "sonner"
+import { Upload, X } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { ProjectSelector } from "@/components/ProjectSelector"
+import { uploadProjectGtcFile, getProjectsWithGtcFiles } from "../service"
+import { type Project } from "@/lib/rfqs/service"
+
+const addProjectSchema = z.object({
+ projectId: z.number().min(1, "프로젝트 선택은 필수입니다."),
+ gtcFile: z.instanceof(File, { message: "GTC 파일은 필수입니다." }).optional(),
+})
+
+type AddProjectFormValues = z.infer<typeof addProjectSchema>
+
+interface AddProjectDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSuccess?: () => void
+}
+
+export function AddProjectDialog({
+ open,
+ onOpenChange,
+ onSuccess,
+}: AddProjectDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [selectedProject, setSelectedProject] = React.useState<Project | null>(null)
+ const [selectedFile, setSelectedFile] = React.useState<File | null>(null)
+ const [excludedProjectIds, setExcludedProjectIds] = React.useState<number[]>([])
+
+ const form = useForm<AddProjectFormValues>({
+ resolver: zodResolver(addProjectSchema),
+ defaultValues: {
+ projectId: 0,
+ gtcFile: undefined,
+ },
+ })
+
+ // 이미 GTC 파일이 등록된 프로젝트 ID 목록 로드
+ React.useEffect(() => {
+ async function loadExcludedProjects() {
+ try {
+ const excludedIds = await getProjectsWithGtcFiles();
+ setExcludedProjectIds(excludedIds);
+ } catch (error) {
+ console.error("제외할 프로젝트 목록 로드 오류:", error);
+ }
+ }
+
+ if (open) {
+ loadExcludedProjects();
+ }
+ }, [open]);
+
+ // 프로젝트 선택 시 폼에 자동으로 채우기
+ const handleProjectSelect = (project: Project) => {
+ // 이미 GTC 파일이 등록된 프로젝트인지 확인
+ if (excludedProjectIds.includes(project.id)) {
+ toast.error("이미 GTC 파일이 등록된 프로젝트입니다.");
+ // 선택된 프로젝트 정보 초기화
+ setSelectedProject(null);
+ form.setValue("projectId", 0);
+ return;
+ }
+
+ setSelectedProject(project)
+ form.setValue("projectId", project.id)
+ }
+
+ // 파일 선택 처리
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ // PDF 파일만 허용
+ if (file.type !== 'application/pdf') {
+ toast.error("PDF 파일만 업로드 가능합니다.")
+ return
+ }
+
+ setSelectedFile(file)
+ form.setValue("gtcFile", file)
+ }
+ }
+
+ // 파일 제거
+ const handleRemoveFile = () => {
+ setSelectedFile(null)
+ form.setValue("gtcFile", undefined)
+ // input 요소의 value도 초기화
+ const fileInput = document.getElementById('gtc-file-input') as HTMLInputElement
+ if (fileInput) {
+ fileInput.value = ''
+ }
+ }
+
+ const onSubmit = async (data: AddProjectFormValues) => {
+ // 프로젝트가 선택되지 않았으면 에러
+ if (!selectedProject) {
+ toast.error("프로젝트를 선택해주세요.")
+ return
+ }
+
+ // 이미 GTC 파일이 등록된 프로젝트인지 다시 한번 확인
+ if (excludedProjectIds.includes(selectedProject.id)) {
+ toast.error("이미 GTC 파일이 등록된 프로젝트입니다.")
+ return
+ }
+
+ // GTC 파일이 없으면 에러
+ if (!data.gtcFile) {
+ toast.error("GTC 파일은 필수입니다.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ // GTC 파일 업로드
+ const fileResult = await uploadProjectGtcFile(selectedProject.id, data.gtcFile)
+
+ if (!fileResult.success) {
+ toast.error(fileResult.error || "GTC 파일 업로드에 실패했습니다.")
+ return
+ }
+
+ toast.success("GTC 파일이 성공적으로 업로드되었습니다.")
+ form.reset()
+ setSelectedProject(null)
+ setSelectedFile(null)
+ onOpenChange(false)
+ onSuccess?.()
+ } catch (error) {
+ console.error("GTC 파일 업로드 오류:", error)
+ toast.error("GTC 파일 업로드 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ form.reset()
+ setSelectedProject(null)
+ setSelectedFile(null)
+ }
+ onOpenChange(newOpen)
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>GTC 파일 추가</DialogTitle>
+ <DialogDescription>
+ 기존 프로젝트를 선택하고 GTC 파일을 업로드합니다. (이미 GTC 파일이 등록된 프로젝트는 제외됩니다)
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ {/* 프로젝트 선택 (필수) */}
+ <FormField
+ control={form.control}
+ name="projectId"
+ render={() => (
+ <FormItem>
+ <FormLabel>프로젝트 선택 *</FormLabel>
+ <FormControl>
+ <ProjectSelector
+ selectedProjectId={selectedProject?.id}
+ onProjectSelect={handleProjectSelect}
+ placeholder="프로젝트를 선택하세요..."
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 선택된 프로젝트 정보 표시 (읽기 전용) */}
+ {selectedProject && (
+ <div className="p-4 bg-muted rounded-lg space-y-2">
+ <h4 className="font-medium text-sm">선택된 프로젝트 정보</h4>
+ <div className="space-y-1 text-sm">
+ <div className="flex justify-between">
+ <span className="text-muted-foreground">프로젝트 코드:</span>
+ <span className="font-medium">{selectedProject.projectCode}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-muted-foreground">프로젝트명:</span>
+ <span className="font-medium">{selectedProject.projectName}</span>
+ </div>
+ </div>
+ </div>
+ )}
+
+ {/* GTC 파일 업로드 */}
+ <FormField
+ control={form.control}
+ name="gtcFile"
+ render={() => (
+ <FormItem>
+ <FormLabel>GTC 파일 *</FormLabel>
+ <div className="space-y-2">
+ {!selectedFile ? (
+ <div className="flex items-center justify-center w-full">
+ <label
+ htmlFor="gtc-file-input"
+ className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100"
+ >
+ <div className="flex flex-col items-center justify-center pt-5 pb-6">
+ <Upload className="w-8 h-8 mb-4 text-gray-500" />
+ <p className="mb-2 text-sm text-gray-500">
+ <span className="font-semibold">클릭하여 파일 선택</span> 또는 드래그 앤 드롭
+ </p>
+ <p className="text-xs text-gray-500">
+ PDF 파일만
+ </p>
+ </div>
+ <input
+ id="gtc-file-input"
+ type="file"
+ className="hidden"
+ accept=".pdf"
+ onChange={handleFileSelect}
+ disabled={isLoading}
+ />
+ </label>
+ </div>
+ ) : (
+ <div className="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
+ <div className="flex items-center space-x-2">
+ <Upload className="w-4 h-4 text-gray-500" />
+ <span className="text-sm font-medium">{selectedFile.name}</span>
+ <span className="text-xs text-gray-500">
+ ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)
+ </span>
+ </div>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={handleRemoveFile}
+ disabled={isLoading}
+ >
+ <X className="w-4 h-4" />
+ </Button>
+ </div>
+ )}
+ </div>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => handleOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading || !selectedProject}>
+ {isLoading ? "업로드 중..." : "GTC 파일 업로드"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/delete-gtc-file-dialog.tsx b/lib/project-gtc/table/delete-gtc-file-dialog.tsx
new file mode 100644
index 00000000..d64be529
--- /dev/null
+++ b/lib/project-gtc/table/delete-gtc-file-dialog.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Trash } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+import { deleteProjectGtcFile } from "../service"
+import { ProjectGtcView } from "@/db/schema"
+
+interface DeleteGtcFileDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ projects: Row<ProjectGtcView>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function DeleteGtcFileDialog({
+ projects,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: DeleteGtcFileDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ try {
+ // 각 프로젝트의 GTC 파일을 삭제
+ const deletePromises = projects.map(project =>
+ deleteProjectGtcFile(project.id)
+ )
+
+ const results = await Promise.all(deletePromises)
+
+ // 성공/실패 확인
+ const successCount = results.filter(result => result.success).length
+ const failureCount = results.length - successCount
+
+ if (failureCount > 0) {
+ toast.error(`${failureCount}개 파일 삭제에 실패했습니다.`)
+ return
+ }
+
+ props.onOpenChange?.(false)
+ toast.success(`${successCount}개 GTC 파일이 성공적으로 삭제되었습니다.`)
+ onSuccess?.()
+ } catch (error) {
+ console.error("Delete error:", error)
+ toast.error("파일 삭제 중 오류가 발생했습니다.")
+ }
+ })
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({projects.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogDescription>
+ This action cannot be undone. This will permanently delete{" "}
+ <span className="font-medium">{projects.length}</span>
+ {projects.length === 1 ? " Project GTC" : " Project GTCs"} from our servers.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DialogClose>
+ <Button
+ aria-label="Delete selected rows"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ Delete
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({projects.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerDescription>
+ This action cannot be undone. This will permanently delete{" "}
+ <span className="font-medium">{projects.length}</span>
+ {projects.length === 1 ? " Project GTC" : " Project GTCs"} from our servers.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DrawerClose>
+ <Button
+ aria-label="Delete selected rows"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ Delete
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/project-gtc-table-columns.tsx b/lib/project-gtc/table/project-gtc-table-columns.tsx
new file mode 100644
index 00000000..dfdf1921
--- /dev/null
+++ b/lib/project-gtc/table/project-gtc-table-columns.tsx
@@ -0,0 +1,364 @@
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Ellipsis, Paperclip, FileText } from "lucide-react"
+import { toast } from "sonner"
+
+import { formatDate, formatDateTime } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { ProjectGtcView } from "@/db/schema"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ProjectGtcView> | null>>
+}
+
+/**
+ * 파일 다운로드 함수
+ */
+const handleFileDownload = async (projectId: number, fileName: string) => {
+ try {
+ // API를 통해 파일 다운로드
+ const response = await fetch(`/api/project-gtc?action=download&projectId=${projectId}`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error('파일 다운로드에 실패했습니다.');
+ }
+
+ // 파일 blob 생성
+ const blob = await response.blob();
+
+ // 다운로드 링크 생성
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 메모리 정리
+ window.URL.revokeObjectURL(url);
+
+ toast.success("파일 다운로드를 시작합니다.");
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+};
+
+/**
+ * tanstack table 컬럼 정의 (중첩 헤더 버전)
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ProjectGtcView>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<ProjectGtcView> = {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) 파일 다운로드 컬럼 (아이콘)
+ // ----------------------------------------------------------------
+ const downloadColumn: ColumnDef<ProjectGtcView> = {
+ id: "download",
+ header: "",
+ cell: ({ row }) => {
+ const project = row.original;
+
+ if (!project.filePath || !project.originalFileName) {
+ return null;
+ }
+
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={() => handleFileDownload(project.id, project.originalFileName!)}
+ title={`${project.originalFileName} 다운로드`}
+ className="hover:bg-muted"
+ >
+ <Paperclip className="h-4 w-4" />
+ <span className="sr-only">다운로드</span>
+ </Button>
+ );
+ },
+ maxSize: 30,
+ enableSorting: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<ProjectGtcView> = {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <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" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ type: "upload", row })}
+ >
+ Edit
+ </DropdownMenuItem>
+
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ type: "delete", row })}
+ >
+ Delete
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
+ // ----------------------------------------------------------------
+ // 4-1) groupMap: { [groupName]: ColumnDef<ProjectGtcView>[] }
+ const groupMap: Record<string, ColumnDef<ProjectGtcView>[]> = {}
+
+ // 프로젝트 정보 그룹
+ groupMap["기본 정보"] = [
+ {
+ accessorKey: "code",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="flex space-x-2">
+ <span className="max-w-[500px] truncate font-medium">
+ {row.getValue("code")}
+ </span>
+ </div>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ minSize: 120,
+ },
+ {
+ accessorKey: "name",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트명" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="flex space-x-2">
+ <span className="max-w-[500px] truncate">
+ {row.getValue("name")}
+ </span>
+ </div>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ minSize: 200,
+ },
+ {
+ accessorKey: "type",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트 타입" />
+ ),
+ cell: ({ row }) => {
+ const type = row.getValue("type") as string
+ return (
+ <div className="flex w-[100px] items-center">
+ <Badge variant="secondary">
+ {type}
+ </Badge>
+ </div>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ minSize: 100,
+ },
+ ]
+
+ // 파일 정보 그룹
+ groupMap["파일 정보"] = [
+ {
+ accessorKey: "originalFileName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="GTC 파일" />
+ ),
+ cell: ({ row }) => {
+ const fileName = row.getValue("originalFileName") as string | null
+ const filePath = row.original.filePath
+
+ if (!fileName) {
+ return (
+ <div className="flex items-center text-muted-foreground">
+ <FileText className="mr-2 h-4 w-4" />
+ <span>파일 없음</span>
+ </div>
+ )
+ }
+
+ return (
+ <div className="flex items-center space-x-2">
+ <FileText className="h-4 w-4" />
+ <div className="flex flex-col">
+ {filePath ? (
+ <button
+ onClick={async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ try {
+ // API를 통해 파일 다운로드
+ const response = await fetch(`/api/project-gtc?action=download&projectId=${row.original.id}`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error('파일을 열 수 없습니다.');
+ }
+
+ // 파일 blob 생성
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ } catch (error) {
+ console.error("파일 미리보기 오류:", error);
+ toast.error("파일을 열 수 없습니다.");
+ }
+ }}
+ className="font-medium text-left hover:underline cursor-pointer text-blue-600 hover:text-blue-800 transition-colors"
+ title="클릭하여 파일 열기"
+ >
+ {fileName}
+ </button>
+ ) : (
+ <span className="font-medium">{fileName}</span>
+ )}
+ </div>
+ </div>
+ )
+ },
+ minSize: 200,
+ },
+ ]
+
+ // 날짜 정보 그룹
+ groupMap["날짜 정보"] = [
+ {
+ accessorKey: "gtcCreatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="GTC 등록일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("gtcCreatedAt") as Date | null
+ if (!date) {
+ return <span className="text-muted-foreground">-</span>
+ }
+ return (
+ <div className="flex items-center">
+ <span>
+ {formatDateTime(new Date(date))}
+ </span>
+ </div>
+ )
+ },
+ minSize: 150,
+ },
+ {
+ accessorKey: "projectCreatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트 생성일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("projectCreatedAt") as Date
+ return (
+ <div className="flex items-center">
+ <span>
+ {formatDate(new Date(date))}
+ </span>
+ </div>
+ )
+ },
+ minSize: 120,
+ },
+ ]
+
+ // ----------------------------------------------------------------
+ // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기
+ // ----------------------------------------------------------------
+ const nestedColumns: ColumnDef<ProjectGtcView>[] = []
+
+ // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함
+ Object.entries(groupMap).forEach(([groupName, colDefs]) => {
+ // 상위 컬럼
+ nestedColumns.push({
+ id: groupName,
+ header: groupName, // "프로젝트 정보", "파일 정보", "날짜 정보" 등
+ columns: colDefs,
+ })
+ })
+
+ // ----------------------------------------------------------------
+ // 5) 최종 컬럼 배열: select, download, nestedColumns, actions
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ downloadColumn, // 다운로드 컬럼 추가
+ ...nestedColumns,
+ actionsColumn,
+ ]
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx b/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx
new file mode 100644
index 00000000..ec6ba053
--- /dev/null
+++ b/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx
@@ -0,0 +1,74 @@
+"use client"
+
+import * as React from "react"
+import { useRouter } from "next/navigation"
+import { type Table } from "@tanstack/react-table"
+import { Download, Plus } from "lucide-react"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+import { DeleteGtcFileDialog } from "./delete-gtc-file-dialog"
+import { AddProjectDialog } from "./add-project-dialog"
+import { ProjectGtcView } from "@/db/schema"
+
+interface ProjectGtcTableToolbarActionsProps {
+ table: Table<ProjectGtcView>
+}
+
+export function ProjectGtcTableToolbarActions({ table }: ProjectGtcTableToolbarActionsProps) {
+ const router = useRouter()
+ const [showAddProjectDialog, setShowAddProjectDialog] = React.useState(false)
+
+ return (
+ <div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteGtcFileDialog
+ projects={table
+ .getFilteredSelectedRowModel()
+ .rows.map((row) => row.original)}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false)
+ router.refresh()
+ }}
+ />
+ ) : null}
+
+ {/** 2) GTC 추가 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setShowAddProjectDialog(true)}
+ className="gap-2"
+ >
+ <Plus className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">GTC 추가</span>
+ </Button>
+
+ {/** 3) Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "project-gtc-list",
+ excludeColumns: ["select", "download", "actions"],
+ })
+ }
+ className="gap-2"
+ >
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
+
+ {/** 4) 프로젝트 추가 다이얼로그 */}
+ <AddProjectDialog
+ open={showAddProjectDialog}
+ onOpenChange={setShowAddProjectDialog}
+ onSuccess={() => {
+ router.refresh()
+ }}
+ />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/project-gtc-table.tsx b/lib/project-gtc/table/project-gtc-table.tsx
new file mode 100644
index 00000000..6e529ccf
--- /dev/null
+++ b/lib/project-gtc/table/project-gtc-table.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import * as React from "react";
+import { useRouter } from "next/navigation";
+import { DataTable } from "@/components/data-table/data-table";
+import { useDataTable } from "@/hooks/use-data-table";
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
+import type {
+ DataTableAdvancedFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { getProjectGtcList } from "../service";
+import { getColumns } from "./project-gtc-table-columns";
+import { DeleteGtcFileDialog } from "./delete-gtc-file-dialog";
+import { UpdateGtcFileSheet } from "./update-gtc-file-sheet";
+import { ProjectGtcTableToolbarActions } from "./project-gtc-table-toolbar-actions";
+import { ProjectGtcView } from "@/db/schema";
+
+interface ProjectGtcTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getProjectGtcList>>,
+ ]
+ >
+}
+
+export function ProjectGtcTable({ promises }: ProjectGtcTableProps) {
+ const router = useRouter();
+ const [rowAction, setRowAction] =
+ React.useState<DataTableRowAction<ProjectGtcView> | null>(null)
+
+ const [{ data, pageCount }] =
+ React.use(promises)
+
+ // 컬럼 설정 - 외부 파일에서 가져옴
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
+
+ // config 기반으로 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<ProjectGtcView>[] = [
+ { id: "code", label: "프로젝트 코드", type: "text" },
+ { id: "name", label: "프로젝트명", type: "text" },
+ {
+ id: "type", label: "프로젝트 타입", type: "select", options: [
+ { label: "Ship", value: "ship" },
+ { label: "Offshore", value: "offshore" },
+ { label: "Other", value: "other" },
+ ]
+ },
+ { id: "originalFileName", label: "GTC 파일명", type: "text" },
+ { id: "projectCreatedAt", label: "프로젝트 생성일", type: "date" },
+ { id: "gtcCreatedAt", label: "GTC 등록일", type: "date" },
+ ];
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "projectCreatedAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ return (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <ProjectGtcTableToolbarActions table={table} />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ <DeleteGtcFileDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ projects={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ router.refresh();
+ }}
+ />
+
+ <UpdateGtcFileSheet
+ open={rowAction?.type === "upload"}
+ onOpenChange={() => setRowAction(null)}
+ project={rowAction && rowAction.type === "upload" ? rowAction.row.original : null}
+ />
+ </>
+ );
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/update-gtc-file-sheet.tsx b/lib/project-gtc/table/update-gtc-file-sheet.tsx
new file mode 100644
index 00000000..65a6bb45
--- /dev/null
+++ b/lib/project-gtc/table/update-gtc-file-sheet.tsx
@@ -0,0 +1,222 @@
+"use client"
+
+import * as React from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import * as z from "zod"
+import { Upload } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Form,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { uploadProjectGtcFile } from "../service"
+import type { ProjectGtcView } from "@/db/schema"
+
+const updateProjectSchema = z.object({
+ gtcFile: z.instanceof(File).optional(),
+})
+
+type UpdateProjectFormValues = z.infer<typeof updateProjectSchema>
+
+interface UpdateGtcFileSheetProps {
+ project: ProjectGtcView | null
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function UpdateGtcFileSheet({
+ project,
+ open,
+ onOpenChange,
+}: UpdateGtcFileSheetProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [selectedFile, setSelectedFile] = React.useState<File | null>(null)
+
+ const form = useForm<UpdateProjectFormValues>({
+ resolver: zodResolver(updateProjectSchema),
+ defaultValues: {
+ gtcFile: undefined,
+ },
+ })
+
+ // 기존 값 세팅 (프로젝트 변경 시)
+ React.useEffect(() => {
+ if (project) {
+ form.reset({
+ gtcFile: undefined,
+ })
+ setSelectedFile(null)
+ }
+ }, [project, form])
+
+ // 파일 선택 처리
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ // PDF 파일만 허용
+ if (file.type !== 'application/pdf') {
+ toast.error("PDF 파일만 업로드 가능합니다.")
+ return
+ }
+ setSelectedFile(file)
+ form.setValue("gtcFile", file)
+ }
+ }
+
+ // 폼 제출 핸들러
+ async function onSubmit(data: UpdateProjectFormValues) {
+ if (!project) {
+ toast.error("프로젝트 정보를 찾을 수 없습니다.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ // GTC 파일이 있으면 업로드
+ if (data.gtcFile) {
+ const fileResult = await uploadProjectGtcFile(project.id, data.gtcFile)
+ if (!fileResult.success) {
+ toast.error(fileResult.error || "GTC 파일 업로드에 실패했습니다.")
+ return
+ }
+ toast.success("GTC 파일이 성공적으로 업로드되었습니다.")
+ } else {
+ toast.info("변경사항이 없습니다.")
+ }
+
+ form.reset()
+ setSelectedFile(null)
+ onOpenChange(false)
+ } catch (error) {
+ console.error("GTC 파일 업로드 오류:", error)
+ toast.error("GTC 파일 업로드 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ if (!project) return null
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-xl">
+ <SheetHeader className="text-left">
+ <SheetTitle>GTC 파일 수정</SheetTitle>
+ <SheetDescription>
+ 프로젝트 정보는 수정할 수 없으며, GTC 파일만 업로드할 수 있습니다.
+ </SheetDescription>
+ </SheetHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ {/* 프로젝트 정보 (읽기 전용) */}
+ <div className="space-y-4">
+ <div>
+ <FormLabel>프로젝트 코드</FormLabel>
+ <Input
+ value={project.code}
+ disabled
+ className="bg-muted"
+ />
+ </div>
+ <div>
+ <FormLabel>프로젝트명</FormLabel>
+ <Input
+ value={project.name}
+ disabled
+ className="bg-muted"
+ />
+ </div>
+ <div>
+ <FormLabel>프로젝트 타입</FormLabel>
+ <Input
+ value={project.type}
+ disabled
+ className="bg-muted"
+ />
+ </div>
+ </div>
+
+ {/* GTC 파일 업로드 */}
+ <FormField
+ control={form.control}
+ name="gtcFile"
+ render={() => (
+ <FormItem>
+ <FormLabel>GTC 파일 (PDF만, 선택 시 기존 파일 교체)</FormLabel>
+ <div className="space-y-2">
+ <label
+ htmlFor="gtc-file-input"
+ className="flex flex-col items-center justify-center w-full min-h-[8rem] border-2 border-dashed border-gray-300 rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100"
+ >
+ <div className="flex flex-col items-center justify-center p-4 text-center">
+ <Upload className="w-8 h-8 mb-2 text-gray-500" />
+ <span className="mb-1 text-base font-semibold text-gray-800">
+ {selectedFile
+ ? selectedFile.name
+ : project.originalFileName
+ ? `현재 파일: ${project.originalFileName}`
+ : "현재 파일 없음"}
+ </span>
+ {selectedFile ? (
+ <span className="text-xs text-gray-500">
+ ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)
+ </span>
+ ) : (
+ <>
+ <p className="mb-2 text-sm text-gray-500">
+ 또는 클릭하여 파일을 선택하세요
+ </p>
+ <p className="text-xs text-gray-500">
+ PDF 파일만
+ </p>
+ </>
+ )}
+ </div>
+ <input
+ id="gtc-file-input"
+ type="file"
+ className="hidden"
+ accept=".pdf"
+ onChange={handleFileSelect}
+ disabled={isLoading}
+ />
+ </label>
+ </div>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading || !selectedFile}>
+ {isLoading ? "업로드 중..." : "GTC 파일 업로드"}
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file
diff --git a/lib/project-gtc/table/view-gtc-file-dialog.tsx b/lib/project-gtc/table/view-gtc-file-dialog.tsx
new file mode 100644
index 00000000..f8cfecd9
--- /dev/null
+++ b/lib/project-gtc/table/view-gtc-file-dialog.tsx
@@ -0,0 +1,230 @@
+"use client"
+
+import * as React from "react"
+import { Download, FileText, Calendar, HardDrive } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { format } from "date-fns"
+import { ko } from "date-fns/locale"
+import type { ProjectGtcView } from "@/db/schema"
+
+interface ViewGtcFileDialogProps {
+ project: ProjectGtcView | null
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+// 파일 크기 포맷팅 함수
+function formatBytes(bytes: number | null): string {
+ if (!bytes) return "0 B"
+
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+export function ViewGtcFileDialog({
+ project,
+ open,
+ onOpenChange,
+}: ViewGtcFileDialogProps) {
+ if (!project || !project.gtcFileId) return null
+
+ const handleDownload = async () => {
+ try {
+ // API를 통해 파일 다운로드
+ const response = await fetch(`/api/project-gtc?action=download&projectId=${project.id}`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error('파일 다운로드에 실패했습니다.');
+ }
+
+ // 파일 blob 생성
+ const blob = await response.blob();
+
+ // 다운로드 링크 생성
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = project.originalFileName || 'gtc-file';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 메모리 정리
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ }
+ }
+
+ const handlePreview = async () => {
+ try {
+ // API를 통해 파일 다운로드
+ const response = await fetch(`/api/project-gtc?action=download&projectId=${project.id}`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error('파일을 열 수 없습니다.');
+ }
+
+ // 파일 blob 생성
+ const blob = await response.blob();
+
+ // PDF 파일인 경우 새 탭에서 열기
+ if (project.mimeType === 'application/pdf') {
+ const url = window.URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ // 메모리 정리는 브라우저가 탭을 닫을 때 자동으로 처리됨
+ } else {
+ // 다른 파일 타입은 다운로드
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = project.originalFileName || 'gtc-file';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ }
+ } catch (error) {
+ console.error("파일 미리보기 오류:", error);
+ }
+ }
+
+ const getFileIcon = () => {
+ if (project.mimeType?.includes('pdf')) {
+ return "📄"
+ } else if (project.mimeType?.includes('word') || project.mimeType?.includes('document')) {
+ return "📝"
+ } else if (project.mimeType?.includes('text')) {
+ return "📃"
+ }
+ return "📎"
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>GTC 파일 정보</DialogTitle>
+ <DialogDescription>
+ 프로젝트 &quot;{project.name}&quot; ({project.code})의 GTC 파일 정보입니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* 프로젝트 정보 */}
+ <div className="p-4 bg-muted rounded-lg">
+ <h4 className="font-medium mb-2">프로젝트 정보</h4>
+ <div className="space-y-2 text-sm">
+ <div className="flex justify-between">
+ <span className="text-muted-foreground">프로젝트 코드:</span>
+ <span className="font-medium">{project.code}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-muted-foreground">프로젝트명:</span>
+ <span className="font-medium">{project.name}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-muted-foreground">프로젝트 타입:</span>
+ <Badge variant="secondary">{project.type}</Badge>
+ </div>
+ </div>
+ </div>
+
+ {/* 파일 정보 */}
+ <div className="p-4 bg-muted rounded-lg">
+ <h4 className="font-medium mb-2">파일 정보</h4>
+ <div className="space-y-3">
+ <div className="flex items-center space-x-3">
+ <span className="text-2xl">{getFileIcon()}</span>
+ <div className="flex-1">
+ <div className="font-medium">{project.originalFileName}</div>
+ <div className="text-sm text-muted-foreground">
+ {project.fileName}
+ </div>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div className="flex items-center space-x-2">
+ <HardDrive className="h-4 w-4 text-muted-foreground" />
+ <span className="text-muted-foreground">파일 크기:</span>
+ <span className="font-medium">
+ {project.fileSize ? formatBytes(project.fileSize) : '알 수 없음'}
+ </span>
+ </div>
+ <div className="flex items-center space-x-2">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <span className="text-muted-foreground">파일 타입:</span>
+ <span className="font-medium">
+ {project.mimeType || '알 수 없음'}
+ </span>
+ </div>
+ <div className="flex items-center space-x-2">
+ <Calendar className="h-4 w-4 text-muted-foreground" />
+ <span className="text-muted-foreground">업로드일:</span>
+ <span className="font-medium">
+ {project.gtcCreatedAt ?
+ format(new Date(project.gtcCreatedAt), "yyyy-MM-dd HH:mm", { locale: ko }) :
+ '알 수 없음'
+ }
+ </span>
+ </div>
+ <div className="flex items-center space-x-2">
+ <Calendar className="h-4 w-4 text-muted-foreground" />
+ <span className="text-muted-foreground">수정일:</span>
+ <span className="font-medium">
+ {project.gtcUpdatedAt ?
+ format(new Date(project.gtcUpdatedAt), "yyyy-MM-dd HH:mm", { locale: ko }) :
+ '알 수 없음'
+ }
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <DialogFooter className="flex space-x-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ >
+ 닫기
+ </Button>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handlePreview}
+ >
+ 미리보기
+ </Button>
+ <Button
+ type="button"
+ onClick={handleDownload}
+ >
+ <Download className="mr-2 h-4 w-4" />
+ 다운로드
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/project-gtc/validations.ts b/lib/project-gtc/validations.ts
new file mode 100644
index 00000000..963ffdd4
--- /dev/null
+++ b/lib/project-gtc/validations.ts
@@ -0,0 +1,32 @@
+import * as z from "zod"
+import { createSearchParamsCache,
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum
+} from "nuqs/server"
+
+import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
+import { ProjectGtcView } from "@/db/schema"
+
+export const projectGtcSearchParamsSchema = createSearchParamsCache({
+ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
+ page: parseAsInteger.withDefault(1),
+ perPage: parseAsInteger.withDefault(10),
+ sort: getSortingStateParser<ProjectGtcView>().withDefault([
+ { id: "projectCreatedAt", desc: true },
+ ]),
+
+ // advanced filter
+ filters: getFiltersStateParser().withDefault([]),
+ joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
+ search: parseAsString.withDefault(""),
+})
+
+export const projectGtcFileSchema = z.object({
+ projectId: z.number().min(1, "프로젝트 ID는 필수입니다."),
+ file: z.instanceof(File).refine((file) => file.size > 0, "파일은 필수입니다."),
+})
+
+export type ProjectGtcSearchParams = Awaited<ReturnType<typeof projectGtcSearchParamsSchema.parse>>
+export type ProjectGtcFileInput = z.infer<typeof projectGtcFileSchema> \ No newline at end of file
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index c3c14aff..96d6a3c9 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -11,7 +11,7 @@ import {
techSalesRfqItems,
biddingProjects
} from "@/db/schema";
-import { and, desc, eq, ilike, or, sql, inArray } from "drizzle-orm";
+import { and, desc, eq, ilike, or, sql, inArray, count, asc } from "drizzle-orm";
import { unstable_cache } from "@/lib/unstable-cache";
import { filterColumns } from "@/lib/filter-columns";
import { getErrorMessage } from "@/lib/handle-error";
@@ -3022,9 +3022,9 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType
try {
// RFQ 타입에 따른 벤더 타입 매핑
- const vendorTypeFilter = rfqType === "SHIP" ? "SHIP" :
- rfqType === "TOP" ? "OFFSHORE_TOP" :
- rfqType === "HULL" ? "OFFSHORE_HULL" : null;
+ const vendorTypeFilter = rfqType === "SHIP" ? "조선" :
+ rfqType === "TOP" ? "해양TOP" :
+ rfqType === "HULL" ? "해양HULL" : null;
const whereConditions = [
eq(techVendors.status, "ACTIVE"),
@@ -3034,9 +3034,9 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType
)
];
- // RFQ 타입이 지정된 경우 벤더 타입 필터링 추가
+ // RFQ 타입이 지정된 경우 벤더 타입 필터링 추가 (컴마 구분 문자열에서 검색)
if (vendorTypeFilter) {
- whereConditions.push(eq(techVendors.techVendorType, vendorTypeFilter));
+ whereConditions.push(sql`${techVendors.techVendorType} LIKE ${'%' + vendorTypeFilter + '%'}`);
}
const results = await db
@@ -3058,4 +3058,237 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType
console.error("Error searching tech vendors:", err);
throw new Error(getErrorMessage(err));
}
+}
+
+/**
+ * Accepted 상태의 Tech Sales Vendor Quotations 조회 (RFQ, Vendor 정보 포함)
+ */
+export async function getAcceptedTechSalesVendorQuotations(input: {
+ search?: string;
+ filters?: Filter<typeof techSalesVendorQuotations>[];
+ sort?: { id: string; desc: boolean }[];
+ page: number;
+ perPage: number;
+ rfqType?: "SHIP" | "TOP" | "HULL";
+}) {
+ unstable_noStore();
+
+ try {
+ const offset = (input.page - 1) * input.perPage;
+
+ // 기본 WHERE 조건: status = 'Accepted'만 조회
+ const baseConditions = [
+ eq(techSalesVendorQuotations.status, 'Accepted')
+ ];
+
+ // 검색 조건 추가
+ const searchConditions = [];
+ if (input.search) {
+ searchConditions.push(
+ ilike(techSalesRfqs.rfqCode, `%${input.search}%`),
+ ilike(techSalesRfqs.description, `%${input.search}%`),
+ ilike(sql`vendors.vendor_name`, `%${input.search}%`),
+ ilike(sql`vendors.vendor_code`, `%${input.search}%`)
+ );
+ }
+
+ // 정렬 조건 변환
+ const orderByConditions: OrderByType[] = [];
+ if (input.sort?.length) {
+ input.sort.forEach((sortItem) => {
+ switch (sortItem.id) {
+ case "rfqCode":
+ orderByConditions.push(sortItem.desc ? desc(techSalesRfqs.rfqCode) : asc(techSalesRfqs.rfqCode));
+ break;
+ case "description":
+ orderByConditions.push(sortItem.desc ? desc(techSalesRfqs.description) : asc(techSalesRfqs.description));
+ break;
+ case "vendorName":
+ orderByConditions.push(sortItem.desc ? desc(sql`vendors.vendor_name`) : asc(sql`vendors.vendor_name`));
+ break;
+ case "vendorCode":
+ orderByConditions.push(sortItem.desc ? desc(sql`vendors.vendor_code`) : asc(sql`vendors.vendor_code`));
+ break;
+ case "totalPrice":
+ orderByConditions.push(sortItem.desc ? desc(techSalesVendorQuotations.totalPrice) : asc(techSalesVendorQuotations.totalPrice));
+ break;
+ case "acceptedAt":
+ orderByConditions.push(sortItem.desc ? desc(techSalesVendorQuotations.acceptedAt) : asc(techSalesVendorQuotations.acceptedAt));
+ break;
+ default:
+ orderByConditions.push(desc(techSalesVendorQuotations.acceptedAt));
+ }
+ });
+ } else {
+ orderByConditions.push(desc(techSalesVendorQuotations.acceptedAt));
+ }
+
+ // 필터 조건 추가
+ const filterConditions = [];
+ if (input.filters?.length) {
+ const { filterWhere, joinOperator } = filterColumns({
+ table: techSalesVendorQuotations,
+ filters: input.filters,
+ joinOperator: input.joinOperator ?? "and",
+ });
+ if (filterWhere) {
+ filterConditions.push(filterWhere);
+ }
+ }
+
+ // RFQ 타입 필터
+ if (input.rfqType) {
+ filterConditions.push(eq(techSalesRfqs.rfqType, input.rfqType));
+ }
+
+ // 모든 조건 결합
+ const allConditions = [
+ ...baseConditions,
+ ...filterConditions,
+ ...(searchConditions.length > 0 ? [or(...searchConditions)] : [])
+ ];
+
+ const whereCondition = allConditions.length > 1
+ ? and(...allConditions)
+ : allConditions[0];
+
+ // 데이터 조회
+ const data = await db
+ .select({
+ // Quotation 정보
+ id: techSalesVendorQuotations.id,
+ rfqId: techSalesVendorQuotations.rfqId,
+ vendorId: techSalesVendorQuotations.vendorId,
+ quotationCode: techSalesVendorQuotations.quotationCode,
+ quotationVersion: techSalesVendorQuotations.quotationVersion,
+ totalPrice: techSalesVendorQuotations.totalPrice,
+ currency: techSalesVendorQuotations.currency,
+ validUntil: techSalesVendorQuotations.validUntil,
+ status: techSalesVendorQuotations.status,
+ remark: techSalesVendorQuotations.remark,
+ submittedAt: techSalesVendorQuotations.submittedAt,
+ acceptedAt: techSalesVendorQuotations.acceptedAt,
+ createdAt: techSalesVendorQuotations.createdAt,
+ updatedAt: techSalesVendorQuotations.updatedAt,
+
+ // RFQ 정보
+ rfqCode: techSalesRfqs.rfqCode,
+ rfqType: techSalesRfqs.rfqType,
+ description: techSalesRfqs.description,
+ dueDate: techSalesRfqs.dueDate,
+ rfqStatus: techSalesRfqs.status,
+ materialCode: techSalesRfqs.materialCode,
+
+ // Vendor 정보
+ vendorName: sql<string>`vendors.vendor_name`,
+ vendorCode: sql<string | null>`vendors.vendor_code`,
+ vendorEmail: sql<string | null>`vendors.email`,
+ vendorCountry: sql<string | null>`vendors.country`,
+
+ // Project 정보
+ projNm: biddingProjects.projNm,
+ pspid: biddingProjects.pspid,
+ sector: biddingProjects.sector,
+ })
+ .from(techSalesVendorQuotations)
+ .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
+ .leftJoin(sql`vendors`, eq(techSalesVendorQuotations.vendorId, sql`vendors.id`))
+ .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id))
+ .where(whereCondition)
+ .orderBy(...orderByConditions)
+ .limit(input.perPage)
+ .offset(offset);
+
+ // 총 개수 조회
+ const totalCount = await db
+ .select({ count: count() })
+ .from(techSalesVendorQuotations)
+ .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
+ .leftJoin(sql`vendors`, eq(techSalesVendorQuotations.vendorId, sql`vendors.id`))
+ .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id))
+ .where(whereCondition);
+
+ const total = totalCount[0]?.count ?? 0;
+ const pageCount = Math.ceil(total / input.perPage);
+
+ return {
+ data,
+ pageCount,
+ total,
+ };
+
+ } catch (error) {
+ console.error("getAcceptedTechSalesVendorQuotations 오류:", error);
+ throw new Error(`Accepted quotations 조회 실패: ${getErrorMessage(error)}`);
+ }
+}
+
+/**
+ * 벤더 견적서 거절 처리 (벤더가 직접 거절)
+ */
+export async function rejectTechSalesVendorQuotations(input: {
+ quotationIds: number[];
+ rejectionReason?: string;
+}) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ throw new Error("인증이 필요합니다.");
+ }
+
+ const result = await db.transaction(async (tx) => {
+ // 견적서들이 존재하고 벤더가 권한이 있는지 확인
+ const quotations = await tx
+ .select({
+ id: techSalesVendorQuotations.id,
+ status: techSalesVendorQuotations.status,
+ vendorId: techSalesVendorQuotations.vendorId,
+ })
+ .from(techSalesVendorQuotations)
+ .where(inArray(techSalesVendorQuotations.id, input.quotationIds));
+
+ if (quotations.length !== input.quotationIds.length) {
+ throw new Error("일부 견적서를 찾을 수 없습니다.");
+ }
+
+ // 이미 거절된 견적서가 있는지 확인
+ const alreadyRejected = quotations.filter(q => q.status === "Rejected");
+ if (alreadyRejected.length > 0) {
+ throw new Error("이미 거절된 견적서가 포함되어 있습니다.");
+ }
+
+ // 승인된 견적서가 있는지 확인
+ const alreadyAccepted = quotations.filter(q => q.status === "Accepted");
+ if (alreadyAccepted.length > 0) {
+ throw new Error("이미 승인된 견적서는 거절할 수 없습니다.");
+ }
+
+ // 견적서 상태를 거절로 변경
+ await tx
+ .update(techSalesVendorQuotations)
+ .set({
+ status: "Rejected",
+ rejectionReason: input.rejectionReason || null,
+ updatedBy: parseInt(session.user.id),
+ updatedAt: new Date(),
+ })
+ .where(inArray(techSalesVendorQuotations.id, input.quotationIds));
+
+ return { success: true, updatedCount: quotations.length };
+ });
+ revalidateTag("techSalesRfqs");
+ revalidateTag("techSalesVendorQuotations");
+ revalidatePath("/partners/techsales/rfq-ship", "page");
+ return {
+ success: true,
+ message: `${result.updatedCount}개의 견적서가 거절되었습니다.`,
+ data: result
+ };
+ } catch (error) {
+ console.error("견적서 거절 오류:", error);
+ return {
+ success: false,
+ error: getErrorMessage(error)
+ };
+ }
} \ No newline at end of file
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
index ddee2317..b89f8953 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
@@ -15,7 +15,8 @@ import {
} from "@/components/ui/tooltip"
import {
TechSalesVendorQuotations,
- TECH_SALES_QUOTATION_STATUS_CONFIG
+ TECH_SALES_QUOTATION_STATUS_CONFIG,
+ TECH_SALES_QUOTATION_STATUSES
} from "@/db/schema"
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
@@ -70,14 +71,21 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
className="translate-y-0.5"
/>
),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
+ cell: ({ row }) => {
+ const isRejected = row.original.status === TECH_SALES_QUOTATION_STATUSES.REJECTED;
+ const isAccepted = row.original.status === TECH_SALES_QUOTATION_STATUSES.ACCEPTED;
+ const isDisabled = isRejected || isAccepted;
+
+ return (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="행 선택"
- className="translate-y-0.5"
- />
- ),
+ className="translate-y-0.5"
+ disabled={isDisabled}
+ />
+ );
+ },
enableSorting: false,
enableHiding: false,
},
@@ -158,33 +166,33 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
// enableSorting: true,
// enableHiding: true,
// },
- {
- accessorKey: "itemName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="자재명" />
- ),
- cell: ({ row }) => {
- const itemName = row.getValue("itemName") as string;
- return (
- <div className="min-w-48 max-w-64">
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <span className="truncate block text-sm">
- {itemName || "N/A"}
- </span>
- </TooltipTrigger>
- <TooltipContent>
- <p className="max-w-xs">{itemName || "N/A"}</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
- );
- },
- enableSorting: true,
- enableHiding: true,
- },
+ // {
+ // accessorKey: "itemName",
+ // header: ({ column }) => (
+ // <DataTableColumnHeaderSimple column={column} title="자재명" />
+ // ),
+ // cell: ({ row }) => {
+ // const itemName = row.getValue("itemName") as string;
+ // return (
+ // <div className="min-w-48 max-w-64">
+ // <TooltipProvider>
+ // <Tooltip>
+ // <TooltipTrigger asChild>
+ // <span className="truncate block text-sm">
+ // {itemName || "N/A"}
+ // </span>
+ // </TooltipTrigger>
+ // <TooltipContent>
+ // <p className="max-w-xs">{itemName || "N/A"}</p>
+ // </TooltipContent>
+ // </Tooltip>
+ // </TooltipProvider>
+ // </div>
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
accessorKey: "projNm",
header: ({ column }) => (
@@ -597,6 +605,9 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
const quotation = row.original;
const rfqCode = quotation.rfqCode || "N/A";
const tooltipText = `${rfqCode} 견적서 작성`;
+ const isRejected = quotation.status === "Rejected";
+ const isAccepted = quotation.status === "Accepted";
+ const isDisabled = isRejected || isAccepted;
return (
<div className="w-16">
@@ -607,16 +618,19 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge
variant="ghost"
size="icon"
onClick={() => {
- router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`);
+ if (!isDisabled) {
+ router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`);
+ }
}}
className="h-8 w-8"
+ disabled={isDisabled}
>
<Edit className="h-4 w-4" />
<span className="sr-only">견적서 작성</span>
</Button>
</TooltipTrigger>
<TooltipContent>
- <p>{tooltipText}</p>
+ <p>{isRejected ? "거절된 견적서는 편집할 수 없습니다" : isAccepted ? "승인된 견적서는 편집할 수 없습니다" : tooltipText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
index 55dcad92..5e5d4f39 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
@@ -12,9 +12,24 @@ import { useRouter } from "next/navigation"
import { getColumns } from "./vendor-quotations-table-columns"
import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet"
import { RfqItemsViewDialog } from "../../table/rfq-items-view-dialog"
-import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service"
+import { getTechSalesRfqAttachments, getVendorQuotations, rejectTechSalesVendorQuotations } from "@/lib/techsales-rfq/service"
import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton"
+import { Button } from "@/components/ui/button"
+import { X } from "lucide-react"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog"
+import { Textarea } from "@/components/ui/textarea"
+import { Label } from "@/components/ui/label"
interface QuotationWithRfqCode extends TechSalesVendorQuotations {
rfqCode?: string | null;
@@ -95,8 +110,6 @@ function TableLoadingSkeleton() {
)
}
-
-
export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTableProps) {
const searchParams = useSearchParams()
const router = useRouter()
@@ -110,6 +123,11 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false)
const [selectedRfqForItems, setSelectedRfqForItems] = React.useState<{ id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; } | null>(null)
+ // 거절 다이얼로그 상태
+ const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false)
+ const [rejectionReason, setRejectionReason] = React.useState("")
+ const [isRejecting, setIsRejecting] = React.useState(false)
+
// 데이터 로딩 상태
const [data, setData] = React.useState<QuotationWithRfqCode[]>([])
const [pageCount, setPageCount] = React.useState(0)
@@ -248,6 +266,54 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
setSelectedRfqForItems(rfq)
setItemsDialogOpen(true)
}, [])
+
+ // 거절 처리 함수
+ const handleRejectQuotations = React.useCallback(async () => {
+ if (!table) return;
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows;
+ const quotationIds = selectedRows.map(row => row.original.id);
+
+ if (quotationIds.length === 0) {
+ toast.error("거절할 견적서를 선택해주세요.");
+ return;
+ }
+
+ // 거절할 수 없는 상태의 견적서가 있는지 확인
+ const invalidStatuses = selectedRows.filter(row =>
+ row.original.status === "Accepted" || row.original.status === "Rejected"
+ );
+
+ if (invalidStatuses.length > 0) {
+ toast.error("이미 승인되었거나 거절된 견적서는 거절할 수 없습니다.");
+ return;
+ }
+
+ setIsRejecting(true);
+
+ try {
+ const result = await rejectTechSalesVendorQuotations({
+ quotationIds,
+ rejectionReason: rejectionReason.trim() || undefined,
+ });
+
+ if (result.success) {
+ toast.success(result.message);
+ setRejectDialogOpen(false);
+ setRejectionReason("");
+ table.resetRowSelection();
+ // 데이터 다시 로드
+ await loadData();
+ } else {
+ toast.error(result.error || "견적서 거절 중 오류가 발생했습니다.");
+ }
+ } catch (error) {
+ console.error("견적서 거절 오류:", error);
+ toast.error("견적서 거절 중 오류가 발생했습니다.");
+ } finally {
+ setIsRejecting(false);
+ }
+ }, [rejectionReason, loadData]);
// 테이블 컬럼 정의
const columns = React.useMemo(() => getColumns({
@@ -322,6 +388,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
enableAdvancedFilter: true,
enableColumnResizing: true,
columnResizeMode: 'onChange',
+ enableRowSelection: true, // 행 선택 활성화
initialState: {
sorting: initialSettings.sort,
columnPinning: { right: ["actions"] },
@@ -366,6 +433,48 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab
filterFields={advancedFilterFields}
shallow={false}
>
+ {/* 선택된 행이 있을 때 거절 버튼 표시 */}
+ {table && table.getFilteredSelectedRowModel().rows.length > 0 && (
+ <AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
+ <AlertDialogTrigger asChild>
+ <Button variant="destructive" size="sm">
+ <X className="mr-2 h-4 w-4" />
+ 선택한 견적서 거절 ({table.getFilteredSelectedRowModel().rows.length}개)
+ </Button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>견적서 거절</AlertDialogTitle>
+ <AlertDialogDescription>
+ 선택한 {table.getFilteredSelectedRowModel().rows.length}개의 견적서를 거절하시겠습니까?
+ 거절된 견적서는 다시 되돌릴 수 없습니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <div className="grid gap-4 py-4">
+ <div className="grid gap-2">
+ <Label htmlFor="rejection-reason">거절 사유 (선택사항)</Label>
+ <Textarea
+ id="rejection-reason"
+ placeholder="거절 사유를 입력하세요..."
+ value={rejectionReason}
+ onChange={(e) => setRejectionReason(e.target.value)}
+ />
+ </div>
+ </div>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleRejectQuotations}
+ disabled={isRejecting}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isRejecting ? "처리 중..." : "거절"}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )}
+
{!isInitialLoad && isLoading && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />