summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
commit5870b73785715d1585531e655c06d8c068eb64ac (patch)
tree1d19e1482f5210cc56e778158b51e810f9717c46
parent95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff)
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
-rw-r--r--app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx95
-rw-r--r--app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx95
-rw-r--r--app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx37
-rw-r--r--app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx18
-rw-r--r--app/api/cron/form-tags-plant/start/route.ts141
-rw-r--r--app/api/cron/form-tags-plant/status/route.ts46
-rw-r--r--app/api/cron/tags-plant/start/route.ts149
-rw-r--r--app/api/cron/tags-plant/status/route.ts46
-rw-r--r--components/client-data-table/data-table.tsx300
-rw-r--r--components/form-data-plant/delete-form-data-dialog.tsx9
-rw-r--r--components/form-data-plant/form-data-report-batch-dialog.tsx21
-rw-r--r--components/form-data-plant/form-data-report-dialog.tsx21
-rw-r--r--components/form-data-plant/form-data-report-temp-upload-dialog.tsx12
-rw-r--r--components/form-data-plant/form-data-report-temp-upload-tab.tsx7
-rw-r--r--components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx19
-rw-r--r--components/form-data-plant/form-data-table.tsx91
-rw-r--r--components/form-data-plant/import-excel-form.tsx13
-rw-r--r--components/form-data-plant/publish-dialog.tsx226
-rw-r--r--components/form-data-plant/spreadJS-dialog.tsx15
-rw-r--r--components/form-data-plant/update-form-sheet.tsx9
-rw-r--r--components/vendor-data-plant/project-swicher.tsx163
-rw-r--r--components/vendor-data-plant/sidebar.tsx479
-rw-r--r--components/vendor-data-plant/vendor-data-container.tsx523
-rw-r--r--db/schema/vendorData.ts87
-rw-r--r--lib/forms-plant/services.ts458
-rw-r--r--lib/forms-plant/stat.ts32
-rw-r--r--lib/sedp/get-form-tags-plant.ts933
-rw-r--r--lib/sedp/get-tags-plant.ts639
-rw-r--r--lib/sedp/sync-form.ts9
-rw-r--r--lib/tags-plant/column-builder.service.ts34
-rw-r--r--lib/tags-plant/queries.ts68
-rw-r--r--lib/tags-plant/repository.ts42
-rw-r--r--lib/tags-plant/service.ts729
-rw-r--r--lib/tags-plant/table/add-tag-dialog.tsx18
-rw-r--r--lib/tags-plant/table/delete-tags-dialog.tsx12
-rw-r--r--lib/tags-plant/table/tag-table.tsx775
-rw-r--r--lib/tags-plant/table/tags-export.tsx5
-rw-r--r--lib/tags-plant/table/tags-table-floating-bar.tsx5
-rw-r--r--lib/tags-plant/table/tags-table-toolbar-actions.tsx42
-rw-r--r--lib/tags-plant/table/update-tag-sheet.tsx13
-rw-r--r--lib/vendor-data/services.ts93
-rw-r--r--lib/vendor-document/service.ts706
-rw-r--r--types/table.d.ts2
43 files changed, 1755 insertions, 5482 deletions
diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx
deleted file mode 100644
index 351fbca3..00000000
--- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx
-import DynamicTable from "@/components/form-data-plant/form-data-table";
-import { getFormData, getFormId,getProjectIdByCode } from "@/lib/forms-plant/services";
-import { useTranslation } from "@/i18n";
-import { Skeleton } from "@/components/ui/skeleton";
-
-interface EngineeringFormPageProps {
- params: {
- lng: string;
- projectCode: string;
- packageCode: string;
- formCode: string;
- };
- searchParams?: {
- mode?: string;
- };
-}
-
-export default async function EngineeringFormPage({
- params,
- searchParams,
-}: EngineeringFormPageProps) {
- // 1) 구조 분해 할당
- const resolvedParams = await params;
-
- // 2) searchParams도 await 필요
- const resolvedSearchParams = await searchParams;
-
- // 3) 구조 분해 할당
- const { lng, projectCode, packageCode, formCode } = resolvedParams;
-
- // i18n 설정
- const { t } = await useTranslation(lng, 'engineering');
-
- // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용)
- const mode = "ENG"; // 기본값은 IM
-
- // 4) DB 조회 - projectCode와 packageCode를 전달
- const { columns, data, editableFieldsMap } = await getFormData(
- formCode,
- projectCode,
- packageCode
- );
-
- // 5) formId 조회 - projectCode와 packageCode를 전달
- const { formId } = await getFormId(projectCode, packageCode, formCode, mode);
-
- const projectId = await getProjectIdByCode(projectCode)
-
- // 6) 예외 처리
- if (!columns) {
- return (
- <p className="text-red-500">
- {t('errors.form_meta_not_found')}
- </p>
- );
- }
-
- // 7) 렌더링
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <div>
- <h3 className="text-lg font-semibold">Engineering Form</h3>
- <p className="text-sm text-muted-foreground">
- Project: {projectCode} / Package: {packageCode} / Form: {formCode}
- </p>
- </div>
- </div>
-
- <div className="space-y-6">
- <DynamicTable
- projectId={projectId}
- projectCode={projectCode}
- packageCode={packageCode}
- formCode={formCode}
- formId={formId}
- columnsJSON={columns}
- dataJSON={data}
- editableFieldsMap={editableFieldsMap}
- mode={"ENG"}
- />
- </div>
- </div>
- );
-}
-
-function TableSkeleton() {
- return (
- <div className="space-y-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-[400px] w-full" />
- </div>
- );
-} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx
deleted file mode 100644
index 29188061..00000000
--- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx
-import DynamicTable from "@/components/form-data-plant/form-data-table";
-import { getFormData, getFormId, getProjectIdByCode } from "@/lib/forms-plant/services";
-import { useTranslation } from "@/i18n";
-import { Skeleton } from "@/components/ui/skeleton";
-
-interface EngineeringFormPageProps {
- params: {
- lng: string;
- projectCode: string;
- packageCode: string;
- formCode: string;
- };
- searchParams?: {
- mode?: string;
- };
-}
-
-export default async function IMFormPage({
- params,
- searchParams,
-}: EngineeringFormPageProps) {
- // 1) 구조 분해 할당
- const resolvedParams = await params;
-
- // 2) searchParams도 await 필요
- const resolvedSearchParams = await searchParams;
-
- // 3) 구조 분해 할당
- const { lng, projectCode, packageCode, formCode } = resolvedParams;
-
- // i18n 설정
- const { t } = await useTranslation(lng, 'engineering');
-
- // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용)
- const mode = "IM"; // 기본값은 IM
-
- // 4) DB 조회 - projectCode와 packageCode를 전달
- const { columns, data, editableFieldsMap } = await getFormData(
- formCode,
- projectCode,
- packageCode
- );
-
- // 5) formId 조회 - projectCode와 packageCode를 전달
- const { formId } = await getFormId(projectCode, packageCode, formCode, mode);
-
- const projectId = await getProjectIdByCode(projectCode)
-
- // 6) 예외 처리
- if (!columns) {
- return (
- <p className="text-red-500">
- {t('errors.form_meta_not_found')}
- </p>
- );
- }
-
- // 7) 렌더링
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <div>
- <h3 className="text-lg font-semibold">Engineering Form</h3>
- <p className="text-sm text-muted-foreground">
- Project: {projectCode} / Package: {packageCode} / Form: {formCode}
- </p>
- </div>
- </div>
-
- <div className="space-y-6">
- <DynamicTable
- projectId={projectId}
- projectCode={projectCode}
- packageCode={packageCode}
- formCode={formCode}
- formId={formId}
- columnsJSON={columns}
- dataJSON={data}
- editableFieldsMap={editableFieldsMap}
- mode={"IM"}
- />
- </div>
- </div>
- );
-}
-
-function TableSkeleton() {
- return (
- <div className="space-y-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-[400px] w-full" />
- </div>
- );
-} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx
deleted file mode 100644
index 4904a8ff..00000000
--- a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/page.tsx
-
-import { TagsTable } from "@/lib/tags-plant/table/tag-table"
-
-interface MasterTagListPageProps {
- params: Promise<{
- lng: string
- projectCode: string
- packageCode: string
- }>
-}
-
-export default async function MasterTagListPage({
- params,
-}: MasterTagListPageProps) {
- const { projectCode, packageCode } = await params
-
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <div>
- <h3 className="text-lg font-semibold">Master Tag List</h3>
- <p className="text-sm text-muted-foreground">
- Project: {projectCode} / Package: {packageCode}
- </p>
- </div>
- </div>
-
- {/* 완전 클라이언트 컴포넌트 */}
- <TagsTable
- projectCode={projectCode}
- packageCode={packageCode}
- formCode="MASTER"
- />
- </div>
- )
-} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx
index 792a3a6a..8a9c43e9 100644
--- a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx
+++ b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx
@@ -2,35 +2,41 @@
import * as React from "react"
import { cookies } from "next/headers"
import { Shell } from "@/components/shell"
+import { getVendorProjectsAndContracts } from "@/lib/vendor-data-plant/services"
import { VendorDataContainer } from "@/components/vendor-data-plant/vendor-data-container"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
import { getServerSession } from "next-auth"
import { InformationButton } from "@/components/information/information-button"
import { useTranslation } from "@/i18n"
-import { getVendorProjectsWithPackages } from "@/lib/vendor-data/services"
interface VendorDataLayoutProps {
children: React.ReactNode
params: { lng?: string }
}
+// Layout 컴포넌트는 서버 컴포넌트입니다
export default async function VendorDataLayout({
children,
params,
}: VendorDataLayoutProps) {
- const { lng } = await params
+ // 기본 언어는 'ko'로 설정, params.locale이 있으면 사용
+ const { lng } = await params;
const language = lng || 'en'
const { t } = await useTranslation(language, 'engineering')
const session = await getServerSession(authOptions)
const vendorId = session?.user.companyId
+ // const vendorId = "17"
const idAsNumber = Number(vendorId)
- // 프로젝트 및 패키지 데이터 가져오기
- const projects = await getVendorProjectsWithPackages(idAsNumber, "plant")
+ // 프로젝트 데이터 가져오기 (type=plant만)
+ const projects = await getVendorProjectsAndContracts(idAsNumber, "plant")
// 레이아웃 설정 쿠키 가져오기
+ // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용
const cookieStore = await cookies()
+
+ // 이제 cookieStore.get() 메서드 사용 가능
const layout = cookieStore.get("react-resizable-panels:layout:mail")
const collapsed = cookieStore.get("react-resizable-panels:collapsed")
@@ -48,6 +54,9 @@ export default async function VendorDataLayout({
</h2>
<InformationButton pagePath="partners/vendor-data-plant" />
</div>
+ {/* <p className="text-muted-foreground">
+ 각종 Data 입력할 수 있습니다
+ </p> */}
</div>
</div>
</div>
@@ -65,6 +74,7 @@ export default async function VendorDataLayout({
defaultCollapsed={defaultCollapsed}
navCollapsedSize={4}
>
+ {/* 페이지별 콘텐츠가 여기에 들어갑니다 */}
{children}
</VendorDataContainer>
)}
diff --git a/app/api/cron/form-tags-plant/start/route.ts b/app/api/cron/form-tags-plant/start/route.ts
deleted file mode 100644
index 17eb8979..00000000
--- a/app/api/cron/form-tags-plant/start/route.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-// app/api/cron/form-tags/start/route.ts
-import { NextRequest } from 'next/server';
-import { v4 as uuidv4 } from 'uuid';
-import { revalidateTag } from 'next/cache';
-
-// 동기화 작업의 상태를 저장할 Map
-// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다
-const syncJobs = new Map<string, {
- status: 'queued' | 'processing' | 'completed' | 'failed';
- startTime: Date;
- endTime?: Date;
- result?: any;
- error?: string;
- progress?: number;
- projectCode?: string;
- formCode?: string;
- packageCode?: string;
- mode?: string
-}>();
-
-export async function POST(request: NextRequest) {
- try {
- // 요청 데이터 가져오기
- let projectCode: string | undefined;
- let formCode: string | undefined;
- let packageCode: string | undefined;
- let mode: string | undefined;
-
-
- const body = await request.json();
- projectCode = body.projectCode;
- formCode = body.formCode;
- packageCode = body.packageCode;
- mode = body.mode; // 모드 정보 추출
-
-
- // 고유 ID 생성
- const syncId = uuidv4();
-
- // 작업 상태 초기화
- syncJobs.set(syncId, {
- status: 'queued',
- startTime: new Date(),
- formCode,
- projectCode,
- packageCode,
- mode
-
- });
-
- // 비동기 작업 시작 (백그라운드에서 실행)
- processTagImport(syncId).catch(error => {
- console.error('Background tag import job failed:', error);
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'failed',
- endTime: new Date(),
- error: error.message || 'Unknown error occurred'
- });
- });
-
- // 즉시 응답 반환 (작업 ID 포함)
- return Response.json({
- success: true,
- message: 'Tag import job started',
- syncId
- }, { status: 200 });
-
- } catch (error: any) {
- console.error('Failed to start tag import job:', error);
- return Response.json({
- success: false,
- error: error.message || 'Failed to start tag import job'
- }, { status: 500 });
- }
-}
-
-// 백그라운드에서 실행되는 태그 가져오기 작업
-async function processTagImport(syncId: string) {
- try {
- const jobInfo = syncJobs.get(syncId)!;
- const formCode = jobInfo.formCode;
- const projectCode = jobInfo.projectCode;
- const packageCode = jobInfo.packageCode || 0;
- const mode = jobInfo.mode || 0;
-
- // 상태 업데이트: 처리 중
- syncJobs.set(syncId, {
- ...jobInfo,
- status: 'processing',
- progress: 0,
- });
-
- if (!formCode || !projectCode ) {
- throw new Error('formCode,projectCode is required');
- }
-
- // 여기서 실제 태그 가져오기 로직 import
- const { importTagsFromSEDP } = await import('@/lib/sedp/get-form-tags-plant');
-
- // 진행 상황 업데이트를 위한 콜백 함수
- const updateProgress = (progress: number) => {
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- progress
- });
- };
-
- // 실제 태그 가져오기 실행
- const result = await importTagsFromSEDP(formCode, projectCode, packageCode, updateProgress);
-
- // 명시적으로 캐시 무효화
- revalidateTag(`forms-${packageCode}-${mode}`);
-
- // 상태 업데이트: 완료
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'completed',
- endTime: new Date(),
- result,
- progress: 100,
- });
-
- return result;
- } catch (error: any) {
- // 에러 발생 시 상태 업데이트
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'failed',
- endTime: new Date(),
- error: error.message || 'Unknown error occurred',
- });
-
- throw error; // 에러 다시 던지기
- }
-}
-
-// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용)
-export function getSyncJobStatus(id: string) {
- return syncJobs.get(id);
-} \ No newline at end of file
diff --git a/app/api/cron/form-tags-plant/status/route.ts b/app/api/cron/form-tags-plant/status/route.ts
deleted file mode 100644
index 9d288f52..00000000
--- a/app/api/cron/form-tags-plant/status/route.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-// app/api/cron/tags/status/route.ts
-import { NextRequest } from 'next/server';
-import { getSyncJobStatus } from '../start/route';
-
-export async function GET(request: NextRequest) {
- try {
- // URL에서 작업 ID 가져오기
- const searchParams = request.nextUrl.searchParams;
- const syncId = searchParams.get('id');
-
- if (!syncId) {
- return Response.json({
- success: false,
- error: 'Missing sync ID parameter'
- }, { status: 400 });
- }
-
- // 작업 상태 조회
- const jobStatus = getSyncJobStatus(syncId);
-
- if (!jobStatus) {
- return Response.json({
- success: false,
- error: 'Sync job not found'
- }, { status: 404 });
- }
-
- // 작업 상태 반환
- return Response.json({
- success: true,
- status: jobStatus.status,
- startTime: jobStatus.startTime,
- endTime: jobStatus.endTime,
- progress: jobStatus.progress,
- result: jobStatus.result,
- error: jobStatus.error
- }, { status: 200 });
-
- } catch (error: any) {
- console.error('Error retrieving tag import status:', error);
- return Response.json({
- success: false,
- error: error.message || 'Failed to retrieve tag import status'
- }, { status: 500 });
- }
-} \ No newline at end of file
diff --git a/app/api/cron/tags-plant/start/route.ts b/app/api/cron/tags-plant/start/route.ts
deleted file mode 100644
index 83e06935..00000000
--- a/app/api/cron/tags-plant/start/route.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-// app/api/cron/tags/start/route.ts
-import { NextRequest } from 'next/server';
-import { v4 as uuidv4 } from 'uuid';
-import { revalidateTag } from 'next/cache';
-
-// 동기화 작업의 상태를 저장할 Map
-// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다
-const syncJobs = new Map<string, {
- status: 'queued' | 'processing' | 'completed' | 'failed';
- startTime: Date;
- endTime?: Date;
- result?: any;
- error?: string;
- progress?: number;
- projectCode?: string;
- packageCode?: string;
- mode?: string
-}>();
-
-export async function POST(request: NextRequest) {
- try {
- // 요청 데이터 가져오기
- let projectCode: string | undefined;
- let packageCode: string | undefined;
- let mode: string | undefined;
-
- try {
- const body = await request.json();
- projectCode = body.projectCode;
- packageCode = body.packageCode;
- mode = body.mode; // 모드 정보 추출
-
- } catch (error) {
- // 요청 본문이 없거나 JSON이 아닌 경우, URL 파라미터 확인
- const searchParams = request.nextUrl.searchParams;
- const projectCodeParam = searchParams.get('projectCode');
- const packageCodeParam = searchParams.get('packageCode');
-
- if (projectCodeParam&&packageCodeParam) {
- projectCode = projectCodeParam;
- packageCode = packageCodeParam;
- }
- mode = searchParams.get('mode') || undefined;
- }
-
- // 고유 ID 생성
- const syncId = uuidv4();
-
- // 작업 상태 초기화
- syncJobs.set(syncId, {
- status: 'queued',
- startTime: new Date(),
- projectCode,
- packageCode,
- mode
- });
-
- // 비동기 작업 시작 (백그라운드에서 실행)
- processTagImport(syncId).catch(error => {
- console.error('Background tag import job failed:', error);
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'failed',
- endTime: new Date(),
- error: error.message || 'Unknown error occurred'
- });
- });
-
- // 즉시 응답 반환 (작업 ID 포함)
- return Response.json({
- success: true,
- message: 'Tag import job started',
- syncId
- }, { status: 200 });
-
- } catch (error: any) {
- console.error('Failed to start tag import job:', error);
- return Response.json({
- success: false,
- error: error.message || 'Failed to start tag import job'
- }, { status: 500 });
- }
-}
-
-// 백그라운드에서 실행되는 태그 가져오기 작업
-async function processTagImport(syncId: string) {
- try {
- const jobInfo = syncJobs.get(syncId)!;
- const projectCode = jobInfo.projectCode;
- const packageCode = jobInfo.packageCode;
- const mode = jobInfo.mode; // 모드 정보 추출
-
-
- // 상태 업데이트: 처리 중
- syncJobs.set(syncId, {
- ...jobInfo,
- status: 'processing',
- progress: 0,
- });
-
- if (!packageCode) {
- throw new Error('Package is required');
- }
-
- // 여기서 실제 태그 가져오기 로직 import
- const { importTagsFromSEDP } = await import('@/lib/sedp/get-tags-plant');
-
- // 진행 상황 업데이트를 위한 콜백 함수
- const updateProgress = (progress: number) => {
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- progress
- });
- };
-
- // 실제 태그 가져오기 실행
- const result = await importTagsFromSEDP(projectCode, packageCode,updateProgress, mode);
-
- // 명시적으로 캐시 무효화
- revalidateTag(`tags-${packageCode}`);
- revalidateTag(`forms-${packageCode}-${mode}`);
-
- // 상태 업데이트: 완료
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'completed',
- endTime: new Date(),
- result,
- progress: 100,
- });
-
- return result;
- } catch (error: any) {
- // 에러 발생 시 상태 업데이트
- syncJobs.set(syncId, {
- ...syncJobs.get(syncId)!,
- status: 'failed',
- endTime: new Date(),
- error: error.message || 'Unknown error occurred',
- });
-
- throw error; // 에러 다시 던지기
- }
-}
-
-// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용)
-export function getSyncJobStatus(id: string) {
- return syncJobs.get(id);
-} \ No newline at end of file
diff --git a/app/api/cron/tags-plant/status/route.ts b/app/api/cron/tags-plant/status/route.ts
deleted file mode 100644
index 9d288f52..00000000
--- a/app/api/cron/tags-plant/status/route.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-// app/api/cron/tags/status/route.ts
-import { NextRequest } from 'next/server';
-import { getSyncJobStatus } from '../start/route';
-
-export async function GET(request: NextRequest) {
- try {
- // URL에서 작업 ID 가져오기
- const searchParams = request.nextUrl.searchParams;
- const syncId = searchParams.get('id');
-
- if (!syncId) {
- return Response.json({
- success: false,
- error: 'Missing sync ID parameter'
- }, { status: 400 });
- }
-
- // 작업 상태 조회
- const jobStatus = getSyncJobStatus(syncId);
-
- if (!jobStatus) {
- return Response.json({
- success: false,
- error: 'Sync job not found'
- }, { status: 404 });
- }
-
- // 작업 상태 반환
- return Response.json({
- success: true,
- status: jobStatus.status,
- startTime: jobStatus.startTime,
- endTime: jobStatus.endTime,
- progress: jobStatus.progress,
- result: jobStatus.result,
- error: jobStatus.error
- }, { status: 200 });
-
- } catch (error: any) {
- console.error('Error retrieving tag import status:', error);
- return Response.json({
- success: false,
- error: error.message || 'Failed to retrieve tag import status'
- }, { status: 500 });
- }
-} \ No newline at end of file
diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx
index 371a1dab..3e009302 100644
--- a/components/client-data-table/data-table.tsx
+++ b/components/client-data-table/data-table.tsx
@@ -49,9 +49,8 @@ interface DataTableProps<TData, TValue> {
children?: React.ReactNode
/** 선택 상태 초기화 트리거 */
clearSelection?: boolean
- initialColumnPinning?: ColumnPinningState
- /** Table 인스턴스를 상위 컴포넌트에 전달하는 콜백 */
- onTableReady?: (table: Table<TData>) => void
+ initialColumnPinning?: ColumnPinningState // 추가
+
}
export function ClientDataTable<TData, TValue>({
@@ -64,8 +63,7 @@ export function ClientDataTable<TData, TValue>({
maxHeight,
onSelectedRowsChange,
clearSelection,
- initialColumnPinning,
- onTableReady
+ initialColumnPinning
}: DataTableProps<TData, TValue>) {
// (1) React Table 상태
@@ -120,13 +118,6 @@ export function ClientDataTable<TData, TValue>({
useAutoSizeColumns(table, autoSizeColumns)
- // 🆕 Table 인스턴스를 상위 컴포넌트에 전달
- React.useEffect(() => {
- if (onTableReady) {
- onTableReady(table)
- }
- }, [table, onTableReady])
-
React.useEffect(() => {
if (!onSelectedRowsChange) return
const selectedRows = table
@@ -173,7 +164,6 @@ export function ClientDataTable<TData, TValue>({
}),
}
}
-
// 🎯 테이블 총 너비 계산
const getTableWidth = React.useCallback(() => {
const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize()
@@ -216,172 +206,174 @@ export function ClientDataTable<TData, TValue>({
{children}
</ClientDataTableAdvancedToolbar>
- <div
- className="max-w-[100vw] overflow-auto"
- style={{ maxHeight: maxHeight || '34rem' }}
- onScroll={handleScroll} // 🎯 스크롤 이벤트 핸들러 추가
- >
- <UiTable
+
+ <div
+ className="max-w-[100vw] overflow-auto"
+ style={{ maxHeight: maxHeight || '34rem' }}
+ onScroll={handleScroll} // 🎯 스크롤 이벤트 핸들러 추가
+ >
+ <UiTable
className={cn(
"[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10",
!hasNestedHeader && "table-fixed" // nested header가 없으면 table-fixed 적용
)}
style={{ minWidth: hasNestedHeader ? getTableWidth() : undefined }}>
- {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */}
- <TableHeader>
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id} className={compactStyles.headerRow}>
- {headerGroup.headers.map((header) => {
- // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음
- if (header.column.getIsGrouped()) {
- return null
- }
+ {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */}
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id} className={compactStyles.headerRow}>
+ {headerGroup.headers.map((header) => {
+ // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음
+ if (header.column.getIsGrouped()) {
+ return null
+ }
- return (
- <TableHead
- key={header.id}
- colSpan={header.colSpan}
- data-column-id={header.column.id}
- className={compactStyles.header}
- style={{
- ...getPinnedStyle(header.column, true), // 🎯 헤더임을 명시
- // 부모 그룹 헤더는 colSpan으로 너비가 결정되므로 width 설정하지 않음
- // 자식 헤더만 개별 width 설정
- ...(!('columns' in header.column.columnDef) && { width: header.getSize() }),
- }}
- >
- <div style={{ position: "relative" }}>
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
+ return (
+ <TableHead
+ key={header.id}
+ colSpan={header.colSpan}
+ data-column-id={header.column.id}
+ className={compactStyles.header}
+ style={{
+ ...getPinnedStyle(header.column, true), // 🎯 헤더임을 명시
+ // 부모 그룹 헤더는 colSpan으로 너비가 결정되므로 width 설정하지 않음
+ // 자식 헤더만 개별 width 설정
+ ...(!('columns' in header.column.columnDef) && { width: header.getSize() }),
+ }}
+ >
+ <div style={{ position: "relative" }}>
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */}
+ {header.column.getCanResize() && !('columns' in header.column.columnDef) && (
+ <DataTableResizer header={header} />
)}
-
- {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */}
- {header.column.getCanResize() && !('columns' in header.column.columnDef) && (
- <DataTableResizer header={header} />
- )}
- </div>
- </TableHead>
- )
- })}
- </TableRow>
- ))}
- </TableHeader>
- <TableBody>
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => {
- // ---------------------------------------------------
- // 1) "그룹핑 헤더" Row인지 확인
- // ---------------------------------------------------
- if (row.getIsGrouped()) {
- // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음
- const groupingColumnId = row.groupingColumnId ?? ""
- const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체
+ </div>
+ </TableHead>
+ )
+ })}
+ </TableRow>
+ ))}
+ </TableHeader>
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => {
+ // ---------------------------------------------------
+ // 1) "그룹핑 헤더" Row인지 확인
+ // ---------------------------------------------------
+ if (row.getIsGrouped()) {
+ // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음
+ const groupingColumnId = row.groupingColumnId ?? ""
+ const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체
- // 컬럼 라벨 가져오기
- let columnLabel = groupingColumnId
- if (groupingColumn) {
- const headerDef = groupingColumn.columnDef.meta?.excelHeader
- if (typeof headerDef === "string") {
- columnLabel = headerDef
+ // 컬럼 라벨 가져오기
+ let columnLabel = groupingColumnId
+ if (groupingColumn) {
+ const headerDef = groupingColumn.columnDef.meta?.excelHeader
+ if (typeof headerDef === "string") {
+ columnLabel = headerDef
+ }
}
+
+ return (
+ <TableRow
+ key={row.id}
+ className={compactStyles.groupRow}
+ data-state={row.getIsExpanded() && "expanded"}
+ >
+ {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */}
+ <TableCell
+ colSpan={table.getVisibleFlatColumns().length}
+ className={compact ? "py-1 px-2" : ""}
+ >
+ {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */}
+ {row.getCanExpand() && (
+ <button
+ onClick={row.getToggleExpandedHandler()}
+ className="inline-flex items-center justify-center mr-2 w-5 h-5"
+ style={{
+ // row.depth: 0이면 top-level, 1이면 그 하위 등
+ marginLeft: `${row.depth * 1.5}rem`,
+ }}
+ >
+ {row.getIsExpanded() ? (
+ <ChevronUp size={compact ? 14 : 16} />
+ ) : (
+ <ChevronRight size={compact ? 14 : 16} />
+ )}
+ </button>
+ )}
+
+ {/* Group Label + 값 */}
+ <span className="font-semibold">
+ {columnLabel}: {row.getValue(groupingColumnId)}
+ </span>
+ <span className="ml-2 text-xs text-muted-foreground">
+ ({row.subRows.length} rows)
+ </span>
+ </TableCell>
+ </TableRow>
+ )
}
+ // ---------------------------------------------------
+ // 2) 일반 Row
+ // → "그룹핑된 컬럼"은 숨긴다
+ // ---------------------------------------------------
return (
<TableRow
key={row.id}
- className={compactStyles.groupRow}
- data-state={row.getIsExpanded() && "expanded"}
+ className={compactStyles.row}
+ data-state={row.getIsSelected() && "selected"}
>
- {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */}
- <TableCell
- colSpan={table.getVisibleFlatColumns().length}
- className={compact ? "py-1 px-2" : ""}
- >
- {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */}
- {row.getCanExpand() && (
- <button
- onClick={row.getToggleExpandedHandler()}
- className="inline-flex items-center justify-center mr-2 w-5 h-5"
+ {row.getVisibleCells().map((cell) => {
+ // 이 셀의 컬럼이 grouped라면 숨긴다
+ if (cell.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <TableCell
+ key={cell.id}
+ data-column-id={cell.column.id}
+ className={compactStyles.cell}
style={{
- // row.depth: 0이면 top-level, 1이면 그 하위 등
- marginLeft: `${row.depth * 1.5}rem`,
+ ...getPinnedStyle(cell.column, false), // 🎯 바디 셀임을 명시
+ width: cell.column.getSize() // 🎯 width 별도 설정
}}
>
- {row.getIsExpanded() ? (
- <ChevronUp size={compact ? 14 : 16} />
- ) : (
- <ChevronRight size={compact ? 14 : 16} />
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
)}
- </button>
- )}
-
- {/* Group Label + 값 */}
- <span className="font-semibold">
- {columnLabel}: {row.getValue(groupingColumnId)}
- </span>
- <span className="ml-2 text-xs text-muted-foreground">
- ({row.subRows.length} rows)
- </span>
- </TableCell>
+ </TableCell>
+ )
+ })}
</TableRow>
)
- }
-
+ })
+ ) : (
// ---------------------------------------------------
- // 2) 일반 Row
- // → "그룹핑된 컬럼"은 숨긴다
+ // 3) 데이터가 없을 때
// ---------------------------------------------------
- return (
- <TableRow
- key={row.id}
- className={compactStyles.row}
- data-state={row.getIsSelected() && "selected"}
+ <TableRow>
+ <TableCell
+ colSpan={table.getAllColumns().length}
+ className={compactStyles.emptyRow + " text-center"}
>
- {row.getVisibleCells().map((cell) => {
- // 이 셀의 컬럼이 grouped라면 숨긴다
- if (cell.column.getIsGrouped()) {
- return null
- }
+ No results.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </UiTable>
+ </div>
- return (
- <TableCell
- key={cell.id}
- data-column-id={cell.column.id}
- className={compactStyles.cell}
- style={{
- ...getPinnedStyle(cell.column, false), // 🎯 바디 셀임을 명시
- width: cell.column.getSize() // 🎯 width 별도 설정
- }}
- >
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
- </TableCell>
- )
- })}
- </TableRow>
- )
- })
- ) : (
- // ---------------------------------------------------
- // 3) 데이터가 없을 때
- // ---------------------------------------------------
- <TableRow>
- <TableCell
- colSpan={table.getAllColumns().length}
- className={compactStyles.emptyRow + " text-center"}
- >
- No results.
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </UiTable>
- </div>
<ClientDataTablePagination table={table} />
</div>
diff --git a/components/form-data-plant/delete-form-data-dialog.tsx b/components/form-data-plant/delete-form-data-dialog.tsx
index 2406407e..6ac8f67c 100644
--- a/components/form-data-plant/delete-form-data-dialog.tsx
+++ b/components/form-data-plant/delete-form-data-dialog.tsx
@@ -40,8 +40,7 @@ interface DeleteFormDataDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
formData: GenericData[]
formCode: string
- projectCode: string
- packageCode: string
+ contractItemId: number
projectId?: number
showTrigger?: boolean
onSuccess?: () => void
@@ -51,8 +50,7 @@ interface DeleteFormDataDialogProps
export function DeleteFormDataDialog({
formData,
formCode,
- projectCode,
- packageCode,
+ contractItemId,
projectId,
showTrigger = true,
onSuccess,
@@ -79,8 +77,7 @@ export function DeleteFormDataDialog({
const result = await deleteFormDataByTags({
formCode,
- projectCode,
- packageCode,
+ contractItemId,
tagIdxs,
projectId,
})
diff --git a/components/form-data-plant/form-data-report-batch-dialog.tsx b/components/form-data-plant/form-data-report-batch-dialog.tsx
index ba41a3c2..24b5827b 100644
--- a/components/form-data-plant/form-data-report-batch-dialog.tsx
+++ b/components/form-data-plant/form-data-report-batch-dialog.tsx
@@ -71,8 +71,7 @@ interface FormDataReportBatchDialogProps {
setOpen: Dispatch<SetStateAction<boolean>>;
columnsJSON: DataTableColumnJSON[];
reportData: ReportData[];
- projectCode: string;
- packageCode: string;
+ packageId: number;
formId: number;
formCode: string;
}
@@ -82,8 +81,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
setOpen,
columnsJSON,
reportData,
- projectCode,
- packageCode,
+ packageId,
formId,
formCode,
}) => {
@@ -102,8 +100,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null);
useEffect(() => {
- updateReportTempList(projectCode, packageCode, formId, setTempList);
- }, [projectCode, packageCode, formId]);
+ updateReportTempList(packageId, formId, setTempList);
+ }, [packageId, formId]);
const onClose = () => {
if (isUploading) {
@@ -363,8 +361,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<PublishDialog
open={publishDialogOpen}
onOpenChange={setPublishDialogOpen}
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={packageId}
formCode={formCode}
fileBlob={generatedFileBlob || undefined}
/>
@@ -412,19 +409,17 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
};
type UpdateReportTempList = (
- projectCode: string,
- packageCode: string,
+ packageId: number,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
) => void;
const updateReportTempList: UpdateReportTempList = async (
- projectCode,
- packageCode,
+ packageId,
formId,
setTempList
) => {
- const tempList = await getReportTempList(projectCode,packageCode, formId);
+ const tempList = await getReportTempList(packageId, formId);
setTempList(
tempList.map((c) => {
diff --git a/components/form-data-plant/form-data-report-dialog.tsx b/components/form-data-plant/form-data-report-dialog.tsx
index 2413fc28..9177ab36 100644
--- a/components/form-data-plant/form-data-report-dialog.tsx
+++ b/components/form-data-plant/form-data-report-dialog.tsx
@@ -49,8 +49,7 @@ interface FormDataReportDialogProps {
columnsJSON: DataTableColumnJSON[];
reportData: ReportData[];
setReportData: Dispatch<SetStateAction<ReportData[]>>;
- projectCode: string;
- packageCode: string;
+ packageId: number;
formId: number;
formCode: string;
}
@@ -59,8 +58,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
columnsJSON,
reportData,
setReportData,
- projectCode,
- packageCode,
+ packageId,
formId,
formCode,
}) => {
@@ -78,8 +76,8 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null);
useEffect(() => {
- updateReportTempList(projectCode, packageCode, formId, setTempList);
- }, [projectCode,packageCode, formId]);
+ updateReportTempList(packageId, formId, setTempList);
+ }, [packageId, formId]);
const onClose = async (value: boolean) => {
if (fileLoading) {
@@ -199,8 +197,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
<PublishDialog
open={publishDialogOpen}
onOpenChange={setPublishDialogOpen}
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={packageId}
formCode={formCode}
fileBlob={generatedFileBlob || undefined}
/>
@@ -397,19 +394,17 @@ const importReportData: ImportReportData = async (
};
type UpdateReportTempList = (
- projectCode: string,
- packageCode: string,
+ packageId: number,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
) => void;
const updateReportTempList: UpdateReportTempList = async (
- projectCode,
- packageCode,
+ packageId,
formId,
setTempList
) => {
- const tempList = await getReportTempList(projectCode,packageCode, formId);
+ const tempList = await getReportTempList(packageId, formId);
setTempList(
tempList.map((c) => {
diff --git a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx
index 66915198..59ea6ade 100644
--- a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx
+++ b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx
@@ -23,8 +23,7 @@ interface FormDataReportTempUploadDialogProps {
columnsJSON: DataTableColumnJSON[];
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
- projectCode: string;
- packageCode: string;
+ packageId: number;
formCode: string;
formId: number;
uploaderType: string;
@@ -36,8 +35,7 @@ export const FormDataReportTempUploadDialog: FC<
columnsJSON,
open,
setOpen,
- projectCode,
- packageCode,
+ packageId,
formId,
formCode,
uploaderType,
@@ -85,16 +83,14 @@ export const FormDataReportTempUploadDialog: FC<
</div>
<TabsContent value="upload">
<FormDataReportTempUploadTab
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={packageId}
formId={formId}
uploaderType={uploaderType}
/>
</TabsContent>
<TabsContent value="uploaded">
<FormDataReportTempUploadedListTab
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={packageId}
formId={formId}
/>
</TabsContent>
diff --git a/components/form-data-plant/form-data-report-temp-upload-tab.tsx b/components/form-data-plant/form-data-report-temp-upload-tab.tsx
index 41466f90..81186ba4 100644
--- a/components/form-data-plant/form-data-report-temp-upload-tab.tsx
+++ b/components/form-data-plant/form-data-report-temp-upload-tab.tsx
@@ -36,15 +36,14 @@ import { uploadReportTemp } from "@/lib/forms-plant/services";
const MAX_FILE_SIZE = 3000000;
interface FormDataReportTempUploadTabProps {
- projectCode: string;
- packageCode: string;
+ packageId: number;
formId: number;
uploaderType: string;
}
export const FormDataReportTempUploadTab: FC<
FormDataReportTempUploadTabProps
-> = ({ projectCode,packageCode, formId, uploaderType }) => {
+> = ({ packageId, formId, uploaderType }) => {
const { toast } = useToast();
const params = useParams();
const lng = (params?.lng as string) || "ko";
@@ -95,7 +94,7 @@ export const FormDataReportTempUploadTab: FC<
formData.append("customFileName", file.name);
formData.append("uploaderType", uploaderType);
- await uploadReportTemp(projectCode, packageCode, formId, formData);
+ await uploadReportTemp(packageId, formId, formData);
successCount++;
setUploadProgress(Math.round((successCount / totalFiles) * 100));
diff --git a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx
index 1b6cefaf..4cfbad69 100644
--- a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx
+++ b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx
@@ -39,14 +39,13 @@ import { getReportTempList, deleteReportTempFile } from "@/lib/forms-plant/servi
import { VendorDataReportTemps } from "@/db/schema/vendorData";
interface FormDataReportTempUploadedListTabProps {
- projectCode: string;
- packageCode: string;
+ packageId: number;
formId: number;
}
export const FormDataReportTempUploadedListTab: FC<
FormDataReportTempUploadedListTabProps
-> = ({ projectCode,packageCode , formId }) => {
+> = ({ packageId, formId }) => {
const params = useParams();
const lng = (params?.lng as string) || "ko";
const { t } = useTranslation(lng, "engineering");
@@ -58,12 +57,12 @@ export const FormDataReportTempUploadedListTab: FC<
useEffect(() => {
const getTempFiles = async () => {
- await updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp);
+ await updateReportTempList(packageId, formId, setPrevReportTemp);
setIsLoading(false);
};
getTempFiles();
- }, [projectCode,packageCode, formId]);
+ }, [packageId, formId]);
return (
<div>
@@ -71,7 +70,7 @@ export const FormDataReportTempUploadedListTab: FC<
<UploadedTempFiles
prevReportTemp={prevReportTemp}
updateReportTempList={() =>
- updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp)
+ updateReportTempList(packageId, formId, setPrevReportTemp)
}
isLoading={isLoading}
t={t}
@@ -81,19 +80,17 @@ export const FormDataReportTempUploadedListTab: FC<
};
type UpdateReportTempList = (
- projectCode: string,
- packageCode: string,
+ packageId: number,
formId: number,
setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>>
) => Promise<void>;
const updateReportTempList: UpdateReportTempList = async (
- projectCode,
- packageCode,
+ packageId,
formId,
setPrevReportTemp
) => {
- const tempList = await getReportTempList(projectCode, packageCode, formId);
+ const tempList = await getReportTempList(packageId, formId);
setPrevReportTemp(tempList);
};
diff --git a/components/form-data-plant/form-data-table.tsx b/components/form-data-plant/form-data-table.tsx
index c6c79a69..30c176bd 100644
--- a/components/form-data-plant/form-data-table.tsx
+++ b/components/form-data-plant/form-data-table.tsx
@@ -76,8 +76,7 @@ interface GenericData {
export interface DynamicTableProps {
dataJSON: GenericData[];
columnsJSON: DataTableColumnJSON[];
- projectCode: string;
- packageCode: string;
+ contractItemId: number;
formCode: string;
formId: number;
projectId: number;
@@ -90,8 +89,7 @@ export interface DynamicTableProps {
export default function DynamicTable({
dataJSON,
columnsJSON,
- projectCode,
- packageCode,
+ contractItemId,
formCode,
formId,
projectId,
@@ -158,8 +156,7 @@ export default function DynamicTable({
// 서버 액션 호출
const result = await excludeFormDataByTags({
formCode,
- projectCode,
- packageCode,
+ contractItemId,
tagNumbers,
});
@@ -291,7 +288,7 @@ export default function DynamicTable({
try {
setIsLoadingStats(true);
// getFormStatusByVendor 서버 액션 직접 호출
- const data = await getFormStatusByVendor(projectId, projectCode, packageCode,formCode);
+ const data = await getFormStatusByVendor(projectId, contractItemId, formCode);
if (data && data.length > 0) {
setFormStats(data[0]);
@@ -342,7 +339,9 @@ export default function DynamicTable({
// SEDP compare dialog state
const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false);
- const projectType = "plant";
+ const [projectCode, setProjectCode] = React.useState<string>('');
+ const [projectType, setProjectType] = React.useState<string>('plant');
+ const [packageCode, setPackageCode] = React.useState<string>('');
// 새로 추가된 Template 다이얼로그 상태
const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false);
@@ -375,13 +374,43 @@ const [isLoadingRegisters, setIsLoadingRegisters] = React.useState(false);
React.useEffect(() => {
const getTempCount = async () => {
- const tempList = await getReportTempList(projectCode, packageCode, formId);
+ const tempList = await getReportTempList(contractItemId, formId);
setTempCount(tempList.length);
};
getTempCount();
- }, [projectCode,packageCode, formId, tempUpDialog]);
+ }, [contractItemId, formId, tempUpDialog]);
+ React.useEffect(() => {
+ const getPackageCode = async () => {
+ try {
+ const packageCode = await getPackageCodeById(contractItemId);
+ setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값
+ } catch (error) {
+ console.error('패키지 조회 실패:', error);
+ setPackageCode('');
+ }
+ };
+
+ getPackageCode();
+ }, [contractItemId])
+ // Get project code when component mounts
+ React.useEffect(() => {
+ const getProjectCode = async () => {
+ try {
+ const project = await getProjectById(projectId);
+ setProjectCode(project.code);
+ setProjectType(project.type);
+ } catch (error) {
+ console.error("Error fetching project code:", error);
+ toast.error("Failed to fetch project code");
+ }
+ };
+
+ if (projectId) {
+ getProjectCode();
+ }
+ }, [projectId]);
// 선택된 행들의 실제 데이터 가져오기
const getSelectedRowsData = React.useCallback(() => {
@@ -500,7 +529,7 @@ React.useEffect(() => {
async function handleSyncTags() {
try {
setIsSyncingTags(true);
- const result = await syncMissingTags(projectCode,packageCode, formCode);
+ const result = await syncMissingTags(contractItemId, formCode);
// Prepare the toast messages based on what changed
const changes = [];
@@ -533,9 +562,9 @@ React.useEffect(() => {
setIsLoadingTags(true);
// API 엔드포인트 호출 - 작업 시작만 요청
- const response = await fetch('/api/cron/form-tags-plant/start', {
+ const response = await fetch('/api/cron/form-tags/start', {
method: 'POST',
- body: JSON.stringify({ projectCode, formCode, packageCode })
+ body: JSON.stringify({ projectCode, formCode, contractItemId })
});
if (!response.ok) {
@@ -574,7 +603,7 @@ React.useEffect(() => {
// 5초마다 상태 확인
pollingRef.current = setInterval(async () => {
try {
- const response = await fetch(`/api/cron/form-tags-plant/status?id=${id}`);
+ const response = await fetch(`/api/cron/form-tags/status?id=${id}`);
if (!response.ok) {
throw new Error('Failed to get tag import status');
@@ -637,8 +666,7 @@ React.useEffect(() => {
tableData,
columnsJSON,
formCode,
- projectCode,
- packageCode,
+ contractItemId,
editableFieldsMap, // 추가: 편집 가능 필드 정보 전달
onPendingChange: setIsImporting, // Let importExcelData handle loading state
onDataUpdate: (newData) => {
@@ -719,8 +747,7 @@ React.useEffect(() => {
const sedpResult = await sendFormDataToSEDP(
formCode, // Send formCode instead of formName
projectId, // Project ID
- projectCode,
- packageCode,
+ contractItemId,
tableData.filter(v=>v.status !== 'excluded'), // Table data
columnsJSON // Column definitions
);
@@ -1199,8 +1226,7 @@ React.useEffect(() => {
columns={columnsJSON}
rowData={rowAction?.row.original ?? null}
formCode={formCode}
- projectCode={projectCode}
- packageCode={packageCode}
+ contractItemId={contractItemId}
editableFieldsMap={editableFieldsMap}
onUpdateSuccess={(updatedValues) => {
// Update the specific row in tableData when a single row is updated
@@ -1218,8 +1244,7 @@ React.useEffect(() => {
<DeleteFormDataDialog
formData={deleteTarget}
formCode={formCode}
- projectCode={projectCode}
- packageCode={packageCode}
+ contractItemId={contractItemId}
projectId={projectId}
open={deleteDialogOpen}
onOpenChange={(open) => {
@@ -1232,6 +1257,16 @@ React.useEffect(() => {
showTrigger={false}
/>
+ {/* Dialog for adding tags */}
+ {/* <AddFormTagDialog
+ projectId={projectId}
+ formCode={formCode}
+ formName={`Form ${formCode}`}
+ contractItemId={contractItemId}
+ packageCode={packageCode}
+ open={addTagDialogOpen}
+ onOpenChange={setAddTagDialogOpen}
+ /> */}
{/* 새로 추가된 Template 다이얼로그 */}
<TemplateViewDialog
@@ -1241,8 +1276,7 @@ React.useEffect(() => {
selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용
tableData={tableData} // SPR_LST_SETUP용 - 새로 추가
formCode={formCode}
- projectCode={projectCode}
- packageCode={packageCode}
+ contractItemId={contractItemId}
editableFieldsMap={editableFieldsMap}
columnsJSON={columnsJSON}
onUpdateSuccess={(updatedValues) => {
@@ -1310,8 +1344,7 @@ React.useEffect(() => {
columnsJSON={columnsJSON}
open={tempUpDialog}
setOpen={setTempUpDialog}
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={contractItemId}
formCode={formCode}
formId={formId}
uploaderType="vendor"
@@ -1323,8 +1356,7 @@ React.useEffect(() => {
columnsJSON={columnsJSON}
reportData={reportData}
setReportData={setReportData}
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={contractItemId}
formCode={formCode}
formId={formId}
/>
@@ -1336,8 +1368,7 @@ React.useEffect(() => {
setOpen={setBatchDownDialog}
columnsJSON={columnsJSON}
reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData}
- projectCode={projectCode}
- packageCode={packageCode}
+ packageId={contractItemId}
formCode={formCode}
formId={formId}
/>
diff --git a/components/form-data-plant/import-excel-form.tsx b/components/form-data-plant/import-excel-form.tsx
index 8ac70c59..ffc6f2f9 100644
--- a/components/form-data-plant/import-excel-form.tsx
+++ b/components/form-data-plant/import-excel-form.tsx
@@ -23,8 +23,7 @@ export interface ImportExcelOptions {
tableData: GenericData[];
columnsJSON: DataTableColumnJSON[];
formCode?: string;
- projectCode: string;
- packageCode: string;
+ contractItemId?: number;
editableFieldsMap?: Map<string, string[]>; // 새로 추가
onPendingChange?: (isPending: boolean) => void;
onDataUpdate?: (updater: ((prev: GenericData[]) => GenericData[]) | GenericData[]) => void;
@@ -219,8 +218,7 @@ export async function importExcelData({
tableData,
columnsJSON,
formCode,
- projectCode,
- packageCode,
+ contractItemId,
editableFieldsMap = new Map(), // 새로 추가
onPendingChange,
onDataUpdate
@@ -529,14 +527,14 @@ export async function importExcelData({
}
});
+ // If formCode and contractItemId are provided, save directly to DB
// importExcelData 함수에서 DB 저장 부분
- if (formCode && projectCode && packageCode) {
+ if (formCode && contractItemId) {
try {
// 배치 업데이트 함수 호출
const result = await updateFormDataBatchInDB(
formCode,
- projectCode,
- packageCode,
+ contractItemId,
importedData // 모든 imported rows를 한번에 전달
);
@@ -635,6 +633,7 @@ export async function importExcelData({
}
} else {
+ // formCode나 contractItemId가 없는 경우 - 로컬 업데이트만
if (onDataUpdate) {
onDataUpdate(() => mergedData);
}
diff --git a/components/form-data-plant/publish-dialog.tsx b/components/form-data-plant/publish-dialog.tsx
index f63c2db8..a3a2ef0b 100644
--- a/components/form-data-plant/publish-dialog.tsx
+++ b/components/form-data-plant/publish-dialog.tsx
@@ -37,21 +37,19 @@ import { Loader2, Check, ChevronsUpDown } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import {
- createSubmissionAction, // 새로운 액션 이름
- fetchDocumentsByProjectAndPackage, // 업데이트된 액션
- fetchStagesByDocumentIdPlant,
- fetchSubmissionsByStageParams, // revisions 대신 submissions
+ createRevisionAction,
+ fetchDocumentsByPackageId,
+ fetchStagesByDocumentId,
+ fetchRevisionsByStageParams,
+ Document,
+ IssueStage,
+ Revision
} from "@/lib/vendor-document/service";
-import type {
- StageDocument,
- StageIssueStage,
-} from "@/db/schema/vendorDocu";
interface PublishDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
- projectCode: string;
- packageCode: string;
+ packageId: number;
formCode: string;
fileBlob?: Blob;
}
@@ -59,8 +57,7 @@ interface PublishDialogProps {
export const PublishDialog: React.FC<PublishDialogProps> = ({
open,
onOpenChange,
- projectCode,
- packageCode,
+ packageId,
formCode,
fileBlob,
}) => {
@@ -68,10 +65,9 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
const { data: session } = useSession();
// State for form data
- const [documents, setDocuments] = useState<StageDocument[]>([]);
- const [stages, setStages] = useState<StageIssueStage[]>([]);
- const [latestRevisionCode, setLatestRevisionCode] = useState<string>("");
- const [latestRevisionNumber, setLatestRevisionNumber] = useState<number>(0);
+ const [documents, setDocuments] = useState<Document[]>([]);
+ const [stages, setStages] = useState<IssueStage[]>([]);
+ const [latestRevision, setLatestRevision] = useState<string>("");
// State for document search
const [openDocumentCombobox, setOpenDocumentCombobox] = useState(false);
@@ -81,10 +77,9 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
const [selectedDocId, setSelectedDocId] = useState<string>("");
const [selectedDocumentDisplay, setSelectedDocumentDisplay] = useState<string>("");
const [selectedStage, setSelectedStage] = useState<string>("");
- const [revisionCodeInput, setRevisionCodeInput] = useState<string>("");
- const [submitterName, setSubmitterName] = useState<string>("");
- const [submissionTitle, setSubmissionTitle] = useState<string>("");
- const [submissionDescription, setSubmissionDescription] = useState<string>("");
+ const [revisionInput, setRevisionInput] = useState<string>("");
+ const [uploaderName, setUploaderName] = useState<string>("");
+ const [comment, setComment] = useState<string>("");
const [customFileName, setCustomFileName] = useState<string>(`${formCode}_document.docx`);
// Loading states
@@ -99,10 +94,10 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
)
: documents;
- // Set submitter name from session when dialog opens
+ // Set uploader name from session when dialog opens
useEffect(() => {
if (open && session?.user?.name) {
- setSubmitterName(session.user.name);
+ setUploaderName(session.user.name);
}
}, [open, session]);
@@ -112,26 +107,24 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
setSelectedDocId("");
setSelectedDocumentDisplay("");
setSelectedStage("");
- setRevisionCodeInput("");
- setSubmissionTitle("");
- setSubmissionDescription("");
- // Only set submitterName if not already set from session
- if (!session?.user?.name) setSubmitterName("");
- setLatestRevisionCode("");
- setLatestRevisionNumber(0);
+ setRevisionInput("");
+ // Only set uploaderName if not already set from session
+ if (!session?.user?.name) setUploaderName("");
+ setComment("");
+ setLatestRevision("");
setCustomFileName(`${formCode}_document.docx`);
setDocumentSearchValue("");
}
}, [open, formCode, session]);
- // Fetch documents based on projectCode and packageCode
+ // Fetch documents based on packageId
useEffect(() => {
async function loadDocuments() {
- if (projectCode && packageCode && open) {
+ if (packageId && open) {
setIsLoading(true);
try {
- const docs = await fetchDocumentsByProjectAndPackage(projectCode, packageCode);
+ const docs = await fetchDocumentsByPackageId(packageId);
setDocuments(docs);
} catch (error) {
console.error("Error fetching documents:", error);
@@ -143,7 +136,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
}
loadDocuments();
- }, [projectCode, packageCode, open]);
+ }, [packageId, open]);
// Fetch stages when document is selected
useEffect(() => {
@@ -153,12 +146,11 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
// Reset dependent fields
setSelectedStage("");
- setRevisionCodeInput("");
- setLatestRevisionCode("");
- setLatestRevisionNumber(0);
+ setRevisionInput("");
+ setLatestRevision("");
try {
- const stagesList = await fetchStagesByDocumentIdPlant(parseInt(selectedDocId, 10));
+ const stagesList = await fetchStagesByDocumentId(parseInt(selectedDocId, 10));
setStages(stagesList);
} catch (error) {
console.error("Error fetching stages:", error);
@@ -174,78 +166,65 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
loadStages();
}, [selectedDocId]);
- // Fetch latest submission (revision) when stage is selected
+ // Fetch latest revision when stage is selected (for reference)
useEffect(() => {
- async function loadLatestSubmission() {
+ async function loadLatestRevision() {
if (selectedDocId && selectedStage) {
setIsLoading(true);
try {
- const submissionsList = await fetchSubmissionsByStageParams(
+ const revsList = await fetchRevisionsByStageParams(
parseInt(selectedDocId, 10),
selectedStage
);
- // Find the latest submission (assuming sorted by revision number)
- if (submissionsList.length > 0) {
- // Sort submissions by revision number descending
- const sortedSubmissions = [...submissionsList].sort((a, b) =>
- b.revisionNumber - a.revisionNumber
- );
+ // Find the latest revision (assuming revisions are sorted by revision number)
+ if (revsList.length > 0) {
+ // Sort revisions if needed
+ const sortedRevisions = [...revsList].sort((a, b) => {
+ return b.revision.localeCompare(a.revision, undefined, { numeric: true });
+ });
- const latestSubmission = sortedSubmissions[0];
- setLatestRevisionCode(latestSubmission.revisionCode);
- setLatestRevisionNumber(latestSubmission.revisionNumber);
+ setLatestRevision(sortedRevisions[0].revision);
- // Auto-increment revision code
- if (latestSubmission.revisionCode.match(/^\d+$/)) {
+ // Pre-fill the revision input with an incremented value if possible
+ if (sortedRevisions[0].revision.match(/^\d+$/)) {
// If it's a number, increment it
- const nextRevision = String(parseInt(latestSubmission.revisionCode, 10) + 1);
- setRevisionCodeInput(nextRevision);
- } else if (latestSubmission.revisionCode.match(/^[A-Za-z]$/)) {
+ const nextRevision = String(parseInt(sortedRevisions[0].revision, 10) + 1);
+ setRevisionInput(nextRevision);
+ } else if (sortedRevisions[0].revision.match(/^[A-Za-z]$/)) {
// If it's a single letter, get the next letter
- const currentChar = latestSubmission.revisionCode.charCodeAt(0);
+ const currentChar = sortedRevisions[0].revision.charCodeAt(0);
const nextChar = String.fromCharCode(currentChar + 1);
- setRevisionCodeInput(nextChar);
- } else if (latestSubmission.revisionCode.toLowerCase().startsWith("rev")) {
- // Handle "Rev0", "Rev1" format
- const numMatch = latestSubmission.revisionCode.match(/\d+$/);
- if (numMatch) {
- const nextNum = parseInt(numMatch[0], 10) + 1;
- setRevisionCodeInput(`Rev${nextNum}`);
- } else {
- setRevisionCodeInput("");
- }
+ setRevisionInput(nextChar);
} else {
// For other formats, just show the latest as reference
- setRevisionCodeInput("");
+ setRevisionInput("");
}
} else {
- // If no submissions exist, set default values
- setLatestRevisionCode("");
- setLatestRevisionNumber(0);
- setRevisionCodeInput("Rev0"); // Start with Rev0
+ // If no revisions exist, set default values
+ setLatestRevision("");
+ setRevisionInput("0");
}
} catch (error) {
- console.error("Error fetching submissions:", error);
- toast.error("Failed to load submission information");
+ console.error("Error fetching revisions:", error);
+ toast.error("Failed to load revision information");
} finally {
setIsLoading(false);
}
} else {
- setLatestRevisionCode("");
- setLatestRevisionNumber(0);
- setRevisionCodeInput("");
+ setLatestRevision("");
+ setRevisionInput("");
}
}
- loadLatestSubmission();
+ loadLatestRevision();
}, [selectedDocId, selectedStage]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!selectedDocId || !selectedStage || !revisionCodeInput || !fileBlob) {
+ if (!selectedDocId || !selectedStage || !revisionInput || !fileBlob) {
toast.error("Please fill in all required fields");
return;
}
@@ -256,30 +235,17 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
// Create FormData
const formData = new FormData();
formData.append("documentId", selectedDocId);
- formData.append("stageName", selectedStage);
- formData.append("revisionCode", revisionCodeInput);
+ formData.append("stage", selectedStage);
+ formData.append("revision", revisionInput);
formData.append("customFileName", customFileName);
+ formData.append("uploaderType", "vendor"); // Default value
- if (submitterName) {
- formData.append("submittedBy", submitterName);
+ if (uploaderName) {
+ formData.append("uploaderName", uploaderName);
}
- if (session?.user?.email) {
- formData.append("submittedByEmail", session.user.email);
- }
-
- if (submissionTitle) {
- formData.append("submissionTitle", submissionTitle);
- }
-
- if (submissionDescription) {
- formData.append("submissionDescription", submissionDescription);
- }
-
- // Get vendor info from selected document
- const selectedDoc = documents.find(doc => doc.id === parseInt(selectedDocId, 10));
- if (selectedDoc) {
- formData.append("vendorId", String(selectedDoc.vendorId));
+ if (comment) {
+ formData.append("comment", comment);
}
// Append file as attachment
@@ -290,14 +256,12 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
formData.append("attachment", file);
}
- // Call server action
- const result = await createSubmissionAction(formData);
+ // Call server action directly
+ const result = await createRevisionAction(formData);
- if (result.success) {
+ if (result) {
toast.success("Document published successfully!");
onOpenChange(false);
- } else {
- toast.error(result.error || "Failed to publish document");
}
} catch (error) {
console.error("Error publishing document:", error);
@@ -337,6 +301,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
className="w-full justify-between"
disabled={isLoading || documents.length === 0}
>
+ {/* Add text-overflow handling for selected document display */}
<span className="truncate">
{selectedDocumentDisplay
? selectedDocumentDisplay
@@ -373,6 +338,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
: "opacity-0"
)}
/>
+ {/* Add text-overflow handling for document items */}
<span className="truncate">{doc.docNumber} - {doc.title}</span>
</CommandItem>
))}
@@ -400,6 +366,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
<SelectContent>
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.stageName}>
+ {/* Add text-overflow handling for stage names */}
<span className="truncate">{stage.stageName}</span>
</SelectItem>
))}
@@ -408,42 +375,28 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
</div>
- {/* Revision Code Input */}
+ {/* Revision Input */}
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="revisionCode" className="text-right">
+ <Label htmlFor="revision" className="text-right">
Revision
</Label>
<div className="col-span-3">
<Input
- id="revisionCode"
- value={revisionCodeInput}
- onChange={(e) => setRevisionCodeInput(e.target.value)}
- placeholder="Enter revision code (e.g., Rev0, A, 1)"
+ id="revision"
+ value={revisionInput}
+ onChange={(e) => setRevisionInput(e.target.value)}
+ placeholder="Enter revision"
disabled={isLoading || !selectedStage}
/>
- {latestRevisionCode && (
+ {latestRevision && (
<p className="text-xs text-muted-foreground mt-1">
- Latest revision: {latestRevisionCode} (#{latestRevisionNumber})
+ Latest revision: {latestRevision}
</p>
)}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="submissionTitle" className="text-right">
- Title
- </Label>
- <div className="col-span-3">
- <Input
- id="submissionTitle"
- value={submissionTitle}
- onChange={(e) => setSubmissionTitle(e.target.value)}
- placeholder="Optional submission title"
- />
- </div>
- </div>
-
- <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="fileName" className="text-right">
File Name
</Label>
@@ -458,15 +411,16 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="submitterName" className="text-right">
- Submitter
+ <Label htmlFor="uploaderName" className="text-right">
+ Uploader
</Label>
<div className="col-span-3">
<Input
- id="submitterName"
- value={submitterName}
- onChange={(e) => setSubmitterName(e.target.value)}
+ id="uploaderName"
+ value={uploaderName}
+ onChange={(e) => setUploaderName(e.target.value)}
placeholder="Your name"
+ // Disable input but show a filled style
className={session?.user?.name ? "opacity-70" : ""}
readOnly={!!session?.user?.name}
/>
@@ -479,15 +433,15 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
</div>
<div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="description" className="text-right">
- Description
+ <Label htmlFor="comment" className="text-right">
+ Comment
</Label>
<div className="col-span-3">
<Textarea
- id="description"
- value={submissionDescription}
- onChange={(e) => setSubmissionDescription(e.target.value)}
- placeholder="Optional submission description"
+ id="comment"
+ value={comment}
+ onChange={(e) => setComment(e.target.value)}
+ placeholder="Optional comment"
className="resize-none"
/>
</div>
@@ -497,7 +451,7 @@ export const PublishDialog: React.FC<PublishDialogProps> = ({
<DialogFooter>
<Button
type="submit"
- disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionCodeInput}
+ disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionInput}
>
{isSubmitting ? (
<>
diff --git a/components/form-data-plant/spreadJS-dialog.tsx b/components/form-data-plant/spreadJS-dialog.tsx
index 9f972676..2eb2c8ba 100644
--- a/components/form-data-plant/spreadJS-dialog.tsx
+++ b/components/form-data-plant/spreadJS-dialog.tsx
@@ -92,8 +92,7 @@ interface TemplateViewDialogProps {
tableData?: GenericData[];
formCode: string;
columnsJSON: DataTableColumnJSON[]
- projectCode: string;
- packageCode: string;
+ contractItemId: number;
editableFieldsMap?: Map<string, string[]>;
onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
}
@@ -143,8 +142,7 @@ export function TemplateViewDialog({
selectedRow,
tableData = [],
formCode,
- projectCode,
- packageCode,
+ contractItemId,
columnsJSON,
editableFieldsMap = new Map(),
onUpdateSuccess
@@ -1437,8 +1435,7 @@ export function TemplateViewDialog({
const { success, message } = await updateFormDataInDB(
formCode,
- projectCode,
- packageCode,
+ contractItemId,
dataToSave
);
@@ -1503,8 +1500,7 @@ export function TemplateViewDialog({
try {
const { success, message } = await updateFormDataInDB(
formCode,
- projectCode,
- packageCode,
+ contractItemId,
dataToSave
);
@@ -1555,8 +1551,7 @@ export function TemplateViewDialog({
selectedRow,
tableData,
formCode,
- projectCode,
- packageCode,
+ contractItemId,
onUpdateSuccess,
cellMappings,
columnsJSON,
diff --git a/components/form-data-plant/update-form-sheet.tsx b/components/form-data-plant/update-form-sheet.tsx
index b7f56f7e..bd75d8f3 100644
--- a/components/form-data-plant/update-form-sheet.tsx
+++ b/components/form-data-plant/update-form-sheet.tsx
@@ -65,8 +65,7 @@ interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef<typeof Shee
columns: DataTableColumnJSON[];
rowData: Record<string, any> | null;
formCode: string;
- projectCode: string;
- packageCode: string;
+ contractItemId: number;
editableFieldsMap?: Map<string, string[]>; // 새로 추가
/** 업데이트 성공 시 호출될 콜백 */
onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
@@ -78,8 +77,7 @@ export function UpdateTagSheet({
columns,
rowData,
formCode,
- projectCode,
- packageCode,
+ contractItemId,
editableFieldsMap = new Map(),
onUpdateSuccess,
...props
@@ -221,8 +219,7 @@ export function UpdateTagSheet({
const { success, message } = await updateFormDataInDB(
formCode,
- projectCode,
- packageCode,
+ contractItemId,
finalValues,
);
diff --git a/components/vendor-data-plant/project-swicher.tsx b/components/vendor-data-plant/project-swicher.tsx
index 9b8f9bea..d3123709 100644
--- a/components/vendor-data-plant/project-swicher.tsx
+++ b/components/vendor-data-plant/project-swicher.tsx
@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
-import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
@@ -17,103 +16,149 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
+import { Check, ChevronsUpDown, Loader2 } from "lucide-react"
-interface PackageData {
- packageCode: string
- packageName: string | null
+interface ContractInfo {
+ contractId: number
+ contractName: string
}
-interface ProjectData {
+interface ProjectInfo {
projectId: number
projectCode: string
projectName: string
- projectType: string
- packages: PackageData[]
+ contracts: ContractInfo[]
}
interface ProjectSwitcherProps {
isCollapsed: boolean
- projects: ProjectData[]
- selectedProjectId: number
- selectedPackageCode: string | null
- onSelectPackage: (projectId: number, packageCode: string) => void
+ projects: ProjectInfo[]
+
+ // 상위가 관리하는 "현재 선택된 contractId"
+ selectedContractId: number | null
+
+ // 콜백: 사용자가 "어떤 contract"를 골랐는지
+ // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함
+ onSelectContract: (projectId: number, contractId: number) => void
+
+ // 로딩 상태 (선택사항)
+ isLoading?: boolean
}
export function ProjectSwitcher({
isCollapsed,
projects,
- selectedProjectId,
- selectedPackageCode,
- onSelectPackage,
+ selectedContractId,
+ onSelectContract,
+ isLoading = false,
}: ProjectSwitcherProps) {
- const [open, setOpen] = React.useState(false)
+ const [popoverOpen, setPopoverOpen] = React.useState(false)
+ const [searchTerm, setSearchTerm] = React.useState("")
- // 현재 선택된 프로젝트와 패키지 정보
- const selectedProject = projects.find(p => p.projectId === selectedProjectId)
- const selectedPackage = selectedProject?.packages.find(
- pkg => pkg.packageCode === selectedPackageCode
- )
+ // 현재 선택된 contract 객체 찾기
+ const selectedContract = React.useMemo(() => {
+ if (!selectedContractId) return null
+ for (const proj of projects) {
+ const found = proj.contracts.find((c) => c.contractId === selectedContractId)
+ if (found) {
+ return { ...found, projectId: proj.projectId, projectName: proj.projectName }
+ }
+ }
+ return null
+ }, [projects, selectedContractId])
+
+ // Trigger label => 계약 이름 or placeholder
+ const triggerLabel = selectedContract?.contractName ?? "Select a contract"
+ // 검색어에 따른 필터링된 프로젝트/계약 목록
+ const filteredProjects = React.useMemo(() => {
+ if (!searchTerm) return projects
+
+ return projects.map(project => ({
+ ...project,
+ contracts: project.contracts.filter(contract =>
+ contract.contractName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ project.projectName.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ })).filter(project => project.contracts.length > 0)
+ }, [projects, searchTerm])
- console.log(projects,"projects")
+ // 계약 선택 핸들러
+ function handleSelectContract(projectId: number, contractId: number) {
+ onSelectContract(projectId, contractId)
+ setPopoverOpen(false)
+ setSearchTerm("") // 검색어 초기화
+ }
- const displayText = selectedPackage
- ? `${selectedProject?.projectCode} - ${selectedPackage.packageCode}`
- : selectedProject?.projectCode || "Select Package"
+ // 총 계약 수 계산 (빈 상태 표시용)
+ const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0)
return (
- <Popover open={open} onOpenChange={setOpen}>
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
<Button
+ type="button"
variant="outline"
- role="combobox"
- aria-expanded={open}
- aria-label="Select a package"
- className={cn("w-full justify-between", isCollapsed && "w-[50px]")}
+ className={cn(
+ "justify-between relative",
+ isCollapsed ? "h-9 w-9 shrink-0 items-center justify-center p-0" : "w-full h-9"
+ )}
+ disabled={isLoading}
+ aria-label="Select Contract"
>
- {isCollapsed ? (
- <ChevronsUpDown className="h-4 w-4" />
+ {isLoading ? (
+ <>
+ <span className={cn(isCollapsed && "hidden")}>Loading...</span>
+ <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} />
+ </>
) : (
<>
- <span className="truncate">{displayText}</span>
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ <span className={cn("truncate flex-grow text-left", isCollapsed && "hidden")}>
+ {triggerLabel}
+ </span>
+ <ChevronsUpDown className={cn("h-4 w-4 opacity-50 flex-shrink-0", isCollapsed && "hidden")} />
</>
)}
</Button>
</PopoverTrigger>
- <PopoverContent className="w-[300px] p-0">
+
+ <PopoverContent className="w-[320px] p-0" align="start">
<Command>
- <CommandInput placeholder="Search package..." />
- <CommandList>
- <CommandEmpty>No package found.</CommandEmpty>
- {projects.map((project) => (
- <CommandGroup key={project.projectId} heading={project.projectName}>
- {project.packages.map((pkg) => (
+ <CommandInput
+ placeholder="Search contracts..."
+ value={searchTerm}
+ onValueChange={setSearchTerm}
+ />
+
+ <CommandList
+ className="max-h-[320px]"
+ onWheel={(e) => {
+ e.stopPropagation() // 이벤트 전파 차단
+ const target = e.currentTarget
+ target.scrollTop += e.deltaY // 직접 스크롤 처리
+ }}
+ >
+ <CommandEmpty>
+ {totalContracts === 0 ? "No contracts found." : "No search results."}
+ </CommandEmpty>
+
+ {filteredProjects.map((project) => (
+ <CommandGroup key={project.projectCode} heading={project.projectName}>
+ {project.contracts.map((contract) => (
<CommandItem
- key={`${project.projectId}-${pkg.packageCode}`}
- onSelect={() => {
- onSelectPackage(project.projectId, pkg.packageCode)
- setOpen(false)
- }}
- className="text-sm"
+ key={contract.contractId}
+ onSelect={() => handleSelectContract(project.projectId, contract.contractId)}
+ value={`${project.projectName} ${contract.contractName}`}
+ className="truncate"
+ title={contract.contractName}
>
+ <span className="truncate">{contract.contractName}</span>
<Check
className={cn(
- "mr-2 h-4 w-4",
- selectedProjectId === project.projectId &&
- selectedPackageCode === pkg.packageCode
- ? "opacity-100"
- : "opacity-0"
+ "ml-auto h-4 w-4 flex-shrink-0",
+ selectedContractId === contract.contractId ? "opacity-100" : "opacity-0"
)}
/>
- <div className="flex flex-col">
- <span className="font-medium">{pkg.packageCode}</span>
- {pkg.packageName && (
- <span className="text-xs text-muted-foreground">
- {pkg.packageName}
- </span>
- )}
- </div>
</CommandItem>
))}
</CommandGroup>
diff --git a/components/vendor-data-plant/sidebar.tsx b/components/vendor-data-plant/sidebar.tsx
index b746e69d..31ee6dc7 100644
--- a/components/vendor-data-plant/sidebar.tsx
+++ b/components/vendor-data-plant/sidebar.tsx
@@ -10,265 +10,304 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip"
-import { List, FormInput, FileText } from "lucide-react"
+import { Package2, FormInput } from "lucide-react"
+import { useRouter, usePathname } from "next/navigation"
import { Skeleton } from "@/components/ui/skeleton"
-import { getEngineeringForms, getIMForms } from "@/lib/tags-plant/service"
+import { type FormInfo } from "@/lib/forms/services"
-interface FormInfo {
- formCode: string
- formName: string
+interface PackageData {
+ itemId: number
+ itemName: string
}
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
isCollapsed: boolean
- selectedPackageCode: string | null
- selectedFormCode: string | null
- currentMode: "master" | "engineering" | "im" | null
- projectCode: string // 추가
- onMasterTagListClick: () => void
- onEngineeringFormClick: (formCode: string) => void
- onIMFormClick: (formCode: string) => void
+ packages: PackageData[]
+ selectedPackageId: number | null
+ selectedProjectId: number | null
+ selectedContractId: number | null
+ onSelectPackage: (itemId: number) => void
+ forms?: FormInfo[]
+ onSelectForm: (formName: string) => void
+ isLoadingForms?: boolean
+ mode: "IM" | "ENG"
}
export function Sidebar({
className,
isCollapsed,
- selectedPackageCode,
- selectedFormCode,
- currentMode,
- projectCode, // 추가
- onMasterTagListClick,
- onEngineeringFormClick,
- onIMFormClick,
+ packages,
+ selectedPackageId,
+ selectedProjectId,
+ selectedContractId,
+ onSelectPackage,
+ forms,
+ onSelectForm,
+ isLoadingForms = false,
+ mode = "IM",
}: SidebarProps) {
- const [engineeringForms, setEngineeringForms] = React.useState<FormInfo[]>([])
- const [imForms, setIMForms] = React.useState<FormInfo[]>([])
- const [isLoadingEngineering, setIsLoadingEngineering] = React.useState(false)
- const [isLoadingIM, setIsLoadingIM] = React.useState(false)
+ const router = useRouter()
+ const rawPathname = usePathname()
+ const pathname = rawPathname ?? ""
- // Engineering 폼 로드
- React.useEffect(() => {
- if (!selectedPackageCode || !projectCode) {
- setEngineeringForms([])
- return
- }
+ /**
+ * ---------------------------
+ * 1) URL에서 현재 패키지 / 폼 코드 추출
+ * ---------------------------
+ */
+ const segments = pathname.split("/").filter(Boolean)
- const loadEngineeringForms = async () => {
- setIsLoadingEngineering(true)
- try {
- const result = await getEngineeringForms(projectCode, selectedPackageCode)
- setEngineeringForms(result)
- } catch (error) {
- console.error("Engineering 폼 로딩 오류:", error)
- setEngineeringForms([])
- } finally {
- setIsLoadingEngineering(false)
- }
- }
+ let currentItemId: number | null = null
+ let currentFormCode: string | null = null
- loadEngineeringForms()
- }, [selectedPackageCode, projectCode])
+ const tagIndex = segments.indexOf("tag")
+ if (tagIndex !== -1 && segments[tagIndex + 1]) {
+ currentItemId = parseInt(segments[tagIndex + 1], 10)
+ }
- // IM 폼 로드
- React.useEffect(() => {
- if (!selectedPackageCode || !projectCode) {
- setIMForms([])
- return
- }
+ const formIndex = segments.indexOf("form")
+ if (formIndex !== -1) {
+ const itemSegment = segments[formIndex + 1]
+ const codeSegment = segments[formIndex + 2]
- const loadIMForms = async () => {
- setIsLoadingIM(true)
- try {
- const result = await getIMForms(projectCode, selectedPackageCode)
- setIMForms(result)
- } catch (error) {
- console.error("IM 폼 로딩 오류:", error)
- setIMForms([])
- } finally {
- setIsLoadingIM(false)
- }
+ if (itemSegment) {
+ currentItemId = parseInt(itemSegment, 10)
+ }
+ if (codeSegment) {
+ currentFormCode = codeSegment
}
+ }
- loadIMForms()
- }, [selectedPackageCode, projectCode])
+ /**
+ * ---------------------------
+ * 2) 패키지 클릭 핸들러 (IM 모드)
+ * ---------------------------
+ */
+ const handlePackageClick = (itemId: number) => {
+ // 상위 컴포넌트 상태 업데이트
+ onSelectPackage(itemId)
- const isMasterActive = currentMode === "master"
- const isPackageSelected = selectedPackageCode !== null
+ // 해당 태그 페이지로 라우팅
+ // 예: /vendor-data-plant/tag/123
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/tag/${itemId}`)
+ }
+
+ /**
+ * ---------------------------
+ * 3) 폼 클릭 핸들러 (IM 모드만 사용)
+ * ---------------------------
+ */
+ const handleFormClick = (form: FormInfo) => {
+ // IM 모드에서만 사용
+ if (selectedPackageId === null) return;
+
+ onSelectForm(form.formName)
+
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/${selectedPackageId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`)
+ }
+
+ /**
+ * ---------------------------
+ * 4) 패키지 클릭 핸들러 (ENG 모드)
+ * ---------------------------
+ */
+ const handlePackageUnderFormClick = (form: FormInfo, pkg: PackageData) => {
+ onSelectForm(form.formName)
+ onSelectPackage(pkg.itemId)
+
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/${pkg.itemId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`)
+ }
return (
<div className={cn("pb-12", className)}>
<div className="space-y-4 py-4">
- {/* Master Tag List */}
- <div className="py-1">
- <h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "M" : "Master"}
- </h2>
- <div className="space-y-1 p-2">
- {isCollapsed ? (
- <Tooltip delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isMasterActive && "bg-accent text-accent-foreground"
- )}
- onClick={onMasterTagListClick}
- disabled={!isPackageSelected}
- >
- <List className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
- Master Tag List
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isMasterActive && "bg-accent text-accent-foreground"
- )}
- onClick={onMasterTagListClick}
- disabled={!isPackageSelected}
- >
- <List className="mr-2 h-4 w-4" />
- Master Tag List
- </Button>
- )}
- </div>
- </div>
+ {/* ---------- 패키지(Items) 목록 - IM 모드에서만 표시 ---------- */}
+ {mode === "IM" && (
+ <>
+ <div className="py-1">
+ <h2 className="relative px-7 text-lg font-semibold tracking-tight">
+ {isCollapsed ? "P" : "Package Lists"}
+ </h2>
+ <ScrollArea className="h-[150px] px-1">
+ <div className="space-y-1 p-2">
+ {packages.map((pkg) => {
+ const isActive = pkg.itemId === currentItemId
- <Separator />
+ return (
+ <div key={pkg.itemId}>
+ {isCollapsed ? (
+ <Tooltip delayDuration={0}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageClick(pkg.itemId)}
+ >
+ <Package2 className="mr-2 h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {pkg.itemName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageClick(pkg.itemId)}
+ >
+ <Package2 className="mr-2 h-4 w-4" />
+ {pkg.itemName}
+ </Button>
+ )}
+ </div>
+ )
+ })}
+ </div>
+ </ScrollArea>
+ </div>
+ <Separator />
+ </>
+ )}
- {/* Engineering Forms */}
+ {/* ---------- 폼 목록 (IM 모드) / 패키지와 폼 목록 (ENG 모드) ---------- */}
<div className="py-1">
<h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "E" : "Engineering"}
+ {isCollapsed
+ ? (mode === "IM" ? "F" : "P")
+ : (mode === "IM" ? "Form Lists" : "Package Lists")
+ }
</h2>
- <ScrollArea className="h-[250px] px-1">
+ <ScrollArea className={cn(
+ "px-1",
+ mode === "IM" ? "h-[300px]" : "h-[450px]"
+ )}>
<div className="space-y-1 p-2">
- {isLoadingEngineering ? (
+ {isLoadingForms ? (
Array.from({ length: 3 }).map((_, index) => (
- <div key={`eng-skeleton-${index}`} className="px-2 py-1.5">
+ <div key={`form-skeleton-${index}`} className="px-2 py-1.5">
<Skeleton className="h-8 w-full" />
</div>
))
- ) : !isPackageSelected ? (
- <p className="text-sm text-muted-foreground px-2">
- Select a package first
- </p>
- ) : engineeringForms.length === 0 ? (
- <p className="text-sm text-muted-foreground px-2">
- No forms available
- </p>
- ) : (
- engineeringForms.map((form) => {
- const isActive =
- currentMode === "engineering" &&
- form.formCode === selectedFormCode
+ ) : mode === "IM" ? (
+ // =========== IM 모드: 폼만 표시 ===========
+ !forms || forms.length === 0 ? (
+ <p className="text-sm text-muted-foreground px-2">
+ (No forms loaded)
+ </p>
+ ) : (
+ forms.map((form) => {
+ const isFormActive = form.formCode === currentFormCode
+ const isDisabled = currentItemId === null
- return isCollapsed ? (
- <Tooltip key={form.formCode} delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onEngineeringFormClick(form.formCode)}
- >
- <FormInput className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
+ return isCollapsed ? (
+ <Tooltip key={form.formCode} delayDuration={0}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isFormActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handleFormClick(form)}
+ disabled={isDisabled}
+ >
+ <FormInput className="mr-2 h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {form.formName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <Button
+ key={form.formCode}
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isFormActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handleFormClick(form)}
+ disabled={isDisabled}
+ >
+ <FormInput className="mr-2 h-4 w-4" />
{form.formName}
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- key={form.formCode}
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onEngineeringFormClick(form.formCode)}
- >
- <FormInput className="mr-2 h-4 w-4" />
- {form.formName}
- </Button>
- )
- })
- )}
- </div>
- </ScrollArea>
- </div>
-
- <Separator />
-
- {/* IM Forms */}
- <div className="py-1">
- <h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "I" : "IM"}
- </h2>
- <ScrollArea className="h-[250px] px-1">
- <div className="space-y-1 p-2">
- {isLoadingIM ? (
- Array.from({ length: 3 }).map((_, index) => (
- <div key={`im-skeleton-${index}`} className="px-2 py-1.5">
- <Skeleton className="h-8 w-full" />
- </div>
- ))
- ) : !isPackageSelected ? (
- <p className="text-sm text-muted-foreground px-2">
- Select a package first
- </p>
- ) : imForms.length === 0 ? (
- <p className="text-sm text-muted-foreground px-2">
- No forms available
- </p>
+ </Button>
+ )
+ })
+ )
) : (
- imForms.map((form) => {
- const isActive =
- currentMode === "im" &&
- form.formCode === selectedFormCode
+ // =========== ENG 모드: 패키지 > 폼 계층 구조 ===========
+ packages.length === 0 ? (
+ <p className="text-sm text-muted-foreground px-2">
+ (No packages loaded)
+ </p>
+ ) : (
+ packages.map((pkg) => (
+ <div key={pkg.itemId} className="space-y-1">
+ {isCollapsed ? (
+ <Tooltip delayDuration={0}>
+ <TooltipTrigger asChild>
+ <div className="px-2 py-1">
+ <Package2 className="h-4 w-4" />
+ </div>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {pkg.itemName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <>
+ {/* 패키지 이름 (클릭 불가능한 라벨) */}
+ <div className="flex items-center px-2 py-1 text-sm font-medium">
+ <Package2 className="mr-2 h-4 w-4" />
+ {pkg.itemName}
+ </div>
+
+ {/* 폼 목록 바로 표시 */}
+ <div className="ml-6 space-y-1">
+ {!forms || forms.length === 0 ? (
+ <p className="text-xs text-muted-foreground px-2 py-1">
+ No forms available
+ </p>
+ ) : (
+ forms.map((form) => {
+ const isFormPackageActive =
+ pkg.itemId === currentItemId &&
+ form.formCode === currentFormCode
- return isCollapsed ? (
- <Tooltip key={form.formCode} delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onIMFormClick(form.formCode)}
- >
- <FileText className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
- {form.formName}
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- key={form.formCode}
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
+ return (
+ <Button
+ key={`${pkg.itemId}-${form.formCode}`}
+ variant="ghost"
+ size="sm"
+ className={cn(
+ "w-full justify-start font-normal text-sm",
+ isFormPackageActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageUnderFormClick(form, pkg)}
+ >
+ <FormInput className="mr-2 h-3 w-3" />
+ {form.formName}
+ </Button>
+ )
+ })
+ )}
+ </div>
+ </>
)}
- onClick={() => onIMFormClick(form.formCode)}
- >
- <FileText className="mr-2 h-4 w-4" />
- {form.formName}
- </Button>
- )
- })
+ </div>
+ ))
+ )
)}
</div>
</ScrollArea>
diff --git a/components/vendor-data-plant/vendor-data-container.tsx b/components/vendor-data-plant/vendor-data-container.tsx
index 7ce831df..60ec2c94 100644
--- a/components/vendor-data-plant/vendor-data-container.tsx
+++ b/components/vendor-data-plant/vendor-data-container.tsx
@@ -4,14 +4,28 @@ import * as React from "react"
import { TooltipProvider } from "@/components/ui/tooltip"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { cn } from "@/lib/utils"
+import { ProjectSwitcher } from "./project-swicher"
import { Sidebar } from "./sidebar"
-import { usePathname, useRouter } from "next/navigation"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services"
import { Separator } from "@/components/ui/separator"
-import { ProjectSwitcher } from "./project-swicher"
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Button } from "@/components/ui/button"
+import { FormInput } from "lucide-react"
+import { Skeleton } from "@/components/ui/skeleton"
+import { selectedModeAtom } from '@/atoms'
+import { useAtom } from 'jotai'
interface PackageData {
- packageCode: string
- packageName: string | null
+ itemId: number
+ itemName: string
+}
+
+interface ContractData {
+ contractId: number
+ contractName: string
+ packages: PackageData[]
}
interface ProjectData {
@@ -19,7 +33,7 @@ interface ProjectData {
projectCode: string
projectName: string
projectType: string
- packages: PackageData[]
+ contracts: ContractData[]
}
interface VendorDataContainerProps {
@@ -30,39 +44,18 @@ interface VendorDataContainerProps {
children: React.ReactNode
}
-function getInfoFromPathname(path: string | null): {
- projectCode: string | null
- packageCode: string | null
- formCode: string | null
- mode: "master" | "engineering" | "im" | null
-} {
- if (!path) return { projectCode: null, packageCode: null, formCode: null, mode: null }
-
- const segments = path.split("/").filter(Boolean)
- const vendorDataIndex = segments.indexOf("vendor-data-plant")
-
- if (vendorDataIndex === -1) {
- return { projectCode: null, packageCode: null, formCode: null, mode: null }
- }
+function getTagIdFromPathname(path: string | null): number | null {
+ if (!path) return null;
- const projectCode = segments[vendorDataIndex + 1] || null
- const packageCode = segments[vendorDataIndex + 2] || null
+ // 태그 패턴 검사 (/tag/123)
+ const tagMatch = path.match(/\/tag\/(\d+)/)
+ if (tagMatch) return parseInt(tagMatch[1], 10)
- // /eng/{formCode} 또는 /im/{formCode} 패턴 체크
- const modeSegment = segments[vendorDataIndex + 3]
- const formCode = segments[vendorDataIndex + 4] || null
+ // 폼 패턴 검사 (/form/123/...)
+ const formMatch = path.match(/\/form\/(\d+)/)
+ if (formMatch) return parseInt(formMatch[1], 10)
- let mode: "master" | "engineering" | "im" | null = null
-
- if (modeSegment === "eng") {
- mode = "engineering"
- } else if (modeSegment === "im") {
- mode = "im"
- } else if (projectCode && packageCode && !modeSegment) {
- mode = "master"
- }
-
- return { projectCode, packageCode, formCode, mode }
+ return null
}
export function VendorDataContainer({
@@ -74,106 +67,267 @@ export function VendorDataContainer({
}: VendorDataContainerProps) {
const pathname = usePathname()
const router = useRouter()
+ const searchParams = useSearchParams()
- // 상태 관리
+ const tagIdNumber = getTagIdFromPathname(pathname)
+
+ // 기본 상태
const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0)
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
- const [selectedPackageCode, setSelectedPackageCode] = React.useState<string | null>(null)
+ const [selectedContractId, setSelectedContractId] = React.useState(
+ projects[0]?.contracts[0]?.contractId || 0
+ )
+ const [selectedPackageId, setSelectedPackageId] = React.useState<number | null>(null)
+ const [formList, setFormList] = React.useState<FormInfo[]>([])
const [selectedFormCode, setSelectedFormCode] = React.useState<string | null>(null)
- const [currentMode, setCurrentMode] = React.useState<"master" | "engineering" | "im" | null>(null)
+ const [isLoadingForms, setIsLoadingForms] = React.useState(false)
- // 현재 선택된 프로젝트
+ console.log(selectedPackageId,"selectedPackageId")
+
+
+ // 현재 선택된 프로젝트/계약/패키지
const currentProject = projects.find((p) => p.projectId === selectedProjectId) ?? projects[0]
+ const currentContract = currentProject?.contracts.find((c) => c.contractId === selectedContractId)
+ ?? currentProject?.contracts[0]
+
+ // 프로젝트 타입 확인 - ship인 경우 항상 ENG 모드
+ const isShipProject = currentProject?.projectType === "ship"
+
+ const [selectedMode, setSelectedMode] = useAtom(selectedModeAtom)
+
+ // URL에서 모드 추출 (ship 프로젝트면 무조건 ENG로, 아니면 URL 또는 기본값)
+ const modeFromUrl = searchParams?.get('mode')
+ const initialMode ="ENG"
+
+ // 모드 초기화 (기존의 useState 초기값 대신)
+ React.useEffect(() => {
+ setSelectedMode(initialMode as "IM" | "ENG")
+ }, [initialMode, setSelectedMode])
+
+ const isTagOrFormRoute = pathname ? (pathname.includes("/tag/") || pathname.includes("/form/")) : false
+ const currentPackageName = isTagOrFormRoute
+ ? currentContract?.packages.find((pkg) => pkg.itemId === selectedPackageId)?.itemName || "None"
+ : "None"
+
+ // 폼 목록에서 고유한 폼 이름만 추출
+ const formNames = React.useMemo(() => {
+ return [...new Set(formList.map((form) => form.formName))]
+ }, [formList])
+
+ // URL에서 현재 폼 코드 추출
+ const getCurrentFormCode = (path: string): string | null => {
+ const segments = path.split("/").filter(Boolean)
+ const formIndex = segments.indexOf("form")
+ if (formIndex !== -1 && segments[formIndex + 2]) {
+ return segments[formIndex + 2]
+ }
+ return null
+ }
+
+ const currentFormCode = React.useMemo(() => {
+ return pathname ? getCurrentFormCode(pathname) : null
+ }, [pathname])
- // URL 변경 시 상태 동기화
+ // URL에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만)
React.useEffect(() => {
- const { projectCode, packageCode, formCode, mode } = getInfoFromPathname(pathname)
-
- if (projectCode && packageCode) {
- // 프로젝트 찾기
- const project = projects.find(p => p.projectCode === projectCode)
- if (project) {
- setSelectedProjectId(project.projectId)
- setSelectedPackageCode(packageCode)
+ if (!isShipProject) {
+ const modeFromUrl = searchParams?.get('mode')
+ if (modeFromUrl === "ENG" || modeFromUrl === "IM") {
+ setSelectedMode(modeFromUrl)
}
}
-
- if (formCode) {
- setSelectedFormCode(formCode)
- } else {
- setSelectedFormCode(null)
+ }, [searchParams, isShipProject])
+
+ // 프로젝트 타입이 변경될 때 모드 업데이트
+ React.useEffect(() => {
+ if (isShipProject) {
+ setSelectedMode("ENG")
+
+ // URL 모드 파라미터도 업데이트
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', 'ENG');
+ router.replace(url.pathname + url.search);
}
-
- if (mode) {
- setCurrentMode(mode)
+ }, [isShipProject, router])
+
+ // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅
+ React.useEffect(() => {
+ if (!currentContract) return
+
+ if (tagIdNumber) {
+ setSelectedPackageId(tagIdNumber)
} else {
- setCurrentMode(null)
+ // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로
+ if (currentContract.packages?.length) {
+ setSelectedPackageId(currentContract.packages[0].itemId)
+ } else {
+ setSelectedPackageId(null)
+ }
}
- }, [pathname, projects])
-
- // 베이스 URL 생성 헬퍼
- const getBaseUrl = () => {
- const segments = pathname?.split("/").filter(Boolean) || []
- const vendorDataIndex = segments.indexOf("vendor-data-plant")
- if (vendorDataIndex === -1) return ""
- return "/" + segments.slice(0, vendorDataIndex + 1).join("/")
- }
+ }, [tagIdNumber, currentContract])
- // 프로젝트 및 패키지 선택 핸들러
- const handleSelectPackage = (projectId: number, packageCode: string) => {
- const project = projects.find(p => p.projectId === projectId)
- if (!project) return
-
- setSelectedProjectId(projectId)
- setSelectedPackageCode(packageCode)
- setSelectedFormCode(null)
- setCurrentMode("master")
+ // (2) 프로젝트 변경 시 계약 초기화
+ // React.useEffect(() => {
+ // if (currentProject?.contracts.length) {
+ // setSelectedContractId(currentProject.contracts[0].contractId)
+ // } else {
+ // setSelectedContractId(0)
+ // }
+ // }, [currentProject])
+
+ // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩
+ React.useEffect(() => {
+ const packageId = getTagIdFromPathname(pathname)
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${packageCode}`)
- }
+ if (packageId) {
+ setSelectedPackageId(packageId)
+
+ // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드
+ loadFormsList(packageId, selectedMode);
+ } else if (currentContract?.packages?.length) {
+ const firstPackageId = currentContract.packages[0].itemId;
+ setSelectedPackageId(firstPackageId);
+ loadFormsList(firstPackageId, selectedMode);
+ }
+ }, [pathname, currentContract, selectedMode])
- // Master Tag List 클릭 핸들러
- const handleMasterTagListClick = () => {
- if (!selectedPackageCode) return
+ // 모드에 따른 폼 로드 함수
+ const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => {
+ if (!packageId) return;
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
+ setIsLoadingForms(true);
+ try {
+ const result = await getFormsByContractItemId(packageId, mode);
+ setFormList(result.forms || []);
+ } catch (error) {
+ console.error(`폼 로딩 오류 (${mode} 모드):`, error);
+ setFormList([]);
+ } finally {
+ setIsLoadingForms(false);
+ }
+ };
+
+ // 핸들러들
+// 수정된 handleSelectContract 함수
+async function handleSelectContract(projId: number, cId: number) {
+ setSelectedProjectId(projId)
+ setSelectedContractId(cId)
+
+ // 선택된 계약의 첫 번째 패키지 찾기
+ const selectedProject = projects.find(p => p.projectId === projId)
+ const selectedContract = selectedProject?.contracts.find(c => c.contractId === cId)
+
+ if (selectedContract?.packages?.length) {
+ const firstPackageId = selectedContract.packages[0].itemId
+ setSelectedPackageId(firstPackageId)
- setCurrentMode("master")
+ // ENG 모드로 폼 목록 로드
+ setIsLoadingForms(true)
+ try {
+ const result = await getFormsByContractItemId(firstPackageId, "ENG")
+ setFormList(result.forms || [])
+
+ // 첫 번째 폼이 있으면 자동 선택 및 네비게이션
+ if (result.forms && result.forms.length > 0) {
+ const firstForm = result.forms[0]
+ setSelectedFormCode(firstForm.formCode)
+
+ // ENG 모드로 설정
+ setSelectedMode("ENG")
+
+ // 첫 번째 폼으로 네비게이션
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${projId}/${cId}?mode=ENG`)
+ } else {
+ // 폼이 없는 경우에도 ENG 모드로 설정
+ setSelectedMode("ENG")
+ setSelectedFormCode(null)
+
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/0/0/${projId}/${cId}?mode=ENG`)
+ }
+ } catch (error) {
+ console.error("폼 로딩 오류:", error)
+ setFormList([])
+ setSelectedFormCode(null)
+
+ // 오류 발생 시에도 ENG 모드로 설정
+ setSelectedMode("ENG")
+ } finally {
+ setIsLoadingForms(false)
+ }
+ } else {
+ // 패키지가 없는 경우
+ setSelectedPackageId(null)
+ setFormList([])
setSelectedFormCode(null)
-
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}`)
+ setSelectedMode("ENG")
}
-
- // Engineering 폼 클릭 핸들러
- const handleEngineeringFormClick = (formCode: string) => {
- if (!selectedPackageCode) return
-
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
-
- setCurrentMode("engineering")
- setSelectedFormCode(formCode)
-
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/eng/${formCode}`)
+}
+
+ function handleSelectPackage(itemId: number) {
+ setSelectedPackageId(itemId)
}
+
+ function handleSelectForm(formName: string) {
+ const form = formList.find((f) => f.formName === formName)
+ if (form) {
+ setSelectedFormCode(form.formCode)
+ }
+ }
+
+ // 모드 변경 핸들러
+// 모드 변경 핸들러
+const handleModeChange = async (mode: "IM" | "ENG") => {
+ // ship 프로젝트인 경우 모드 변경 금지
+ if (isShipProject && mode !== "ENG") return;
- // IM 폼 클릭 핸들러
- const handleIMFormClick = (formCode: string) => {
- if (!selectedPackageCode) return
-
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
-
- setCurrentMode("im")
- setSelectedFormCode(formCode)
+ setSelectedMode(mode);
+
+ // 모드가 변경될 때 자동 네비게이션
+ if (currentContract?.packages?.length) {
+ const firstPackageId = currentContract.packages[0].itemId;
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/im/${formCode}`)
+ if (mode === "IM") {
+ // IM 모드: 첫 번째 패키지로 이동
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/tag/${firstPackageId}?mode=${mode}`);
+ } else {
+ // ENG 모드: 폼 목록을 먼저 로드
+ setIsLoadingForms(true);
+ try {
+ const result = await getFormsByContractItemId(firstPackageId, mode);
+ setFormList(result.forms || []);
+
+ // 폼이 있으면 첫 번째 폼으로 이동
+ if (result.forms && result.forms.length > 0) {
+ const firstForm = result.forms[0];
+ setSelectedFormCode(firstForm.formCode);
+
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ } else {
+ // 폼이 없으면 모드만 변경
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/form/0/0/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ }
+ } catch (error) {
+ console.error(`폼 로딩 오류 (${mode} 모드):`, error);
+ // 오류 발생 시 모드만 변경
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', mode);
+ router.replace(url.pathname + url.search);
+ } finally {
+ setIsLoadingForms(false);
+ }
+ }
+ } else {
+ // 패키지가 없는 경우, 모드만 변경
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', mode);
+ router.replace(url.pathname + url.search);
}
+};
return (
<TooltipProvider delayDuration={0}>
@@ -197,28 +351,151 @@ export function VendorDataContainer({
<ProjectSwitcher
isCollapsed={isCollapsed}
projects={projects}
- selectedProjectId={selectedProjectId}
- selectedPackageCode={selectedPackageCode}
- onSelectPackage={handleSelectPackage}
+ selectedContractId={selectedContractId}
+ onSelectContract={handleSelectContract}
/>
</div>
<Separator />
- <Sidebar
- isCollapsed={isCollapsed}
- selectedPackageCode={selectedPackageCode}
- selectedFormCode={selectedFormCode}
- currentMode={currentMode}
- onMasterTagListClick={handleMasterTagListClick}
- onEngineeringFormClick={handleEngineeringFormClick}
- onIMFormClick={handleIMFormClick}
- />
+ {!isCollapsed ? (
+ isShipProject ? (
+ // 프로젝트 타입이 ship인 경우: 탭 없이 ENG 모드 사이드바만 바로 표시
+ <div className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedProjectId={selectedProjectId}
+ selectedContractId={selectedContractId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="ENG"
+ className="hidden lg:block"
+ />
+ </div>
+ ) : (
+ // 프로젝트 타입이 ship이 아닌 경우: 기존 탭 UI 표시
+ <Tabs
+ defaultValue={initialMode}
+ value={selectedMode}
+ onValueChange={(value) => handleModeChange(value as "IM" | "ENG")}
+ className="w-full"
+ >
+ <TabsList className="w-full">
+ <TabsTrigger value="ENG" className="flex-1">Engineering</TabsTrigger>
+ <TabsTrigger value="IM" className="flex-1">Handover</TabsTrigger>
+
+ </TabsList>
+
+ <TabsContent value="IM" className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedContractId={selectedContractId}
+ selectedProjectId={selectedProjectId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="IM"
+ className="hidden lg:block"
+ />
+ </TabsContent>
+
+ <TabsContent value="ENG" className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedContractId={selectedContractId}
+ selectedProjectId={selectedProjectId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="ENG"
+ className="hidden lg:block"
+ />
+ </TabsContent>
+ </Tabs>
+ )
+ ) : (
+ // 접혀있을 때 UI
+ <>
+ {!isShipProject && (
+ // ship 프로젝트가 아닐 때만 모드 선택 버튼 표시
+ <div className="flex justify-center space-x-1 my-2">
+
+ <Button
+ variant={selectedMode === "ENG" ? "default" : "ghost"}
+ size="sm"
+ className="h-8 px-2"
+ onClick={() => handleModeChange("ENG")}
+ >
+ Engineering
+ </Button>
+ <Button
+ variant={selectedMode === "IM" ? "default" : "ghost"}
+ size="sm"
+ className="h-8 px-2"
+ onClick={() => handleModeChange("IM")}
+ >
+ Handover
+ </Button>
+ </div>
+ )}
+
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedProjectId={selectedProjectId}
+ selectedContractId={selectedContractId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode={isShipProject ? "ENG" : selectedMode}
+ className="hidden lg:block"
+ />
+ </>
+ )}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={40}>
<div className="p-4 h-full overflow-auto flex flex-col">
+ <div className="flex items-center justify-between mb-4">
+ <h2 className="text-lg font-bold">
+ {isShipProject || selectedMode === "ENG"
+ ? "Engineering Mode"
+ : `Package: ${currentPackageName}`}
+ </h2>
+ </div>
{children}
</div>
</ResizablePanel>
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index 5301e61a..c3df6b53 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -41,27 +41,6 @@ export const forms = pgTable("forms", {
}
})
-export const formsPlant = pgTable("forms_plant", {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- projectCode: varchar("project_code", { length: 100 }).notNull(),
- packageCode: varchar("package_code", { length: 100 }).notNull(),
- formCode: varchar("form_code", { length: 100 }).notNull(),
- formName: varchar("form_name", { length: 255 }).notNull(),
- // source: varchar("source", { length: 255 }),
- // 새로 추가된 칼럼: eng와 im
- eng: boolean("eng").default(false).notNull(),
- im: boolean("im").default(false).notNull(),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
-}, (table) => {
- return {
- projectItemFormCodeUnique: uniqueIndex("project_item_form_code_unique").on(
- table.projectCode,
- table.formCode
- ),
- }
-})
-
// formMetas에 projectId 추가
export const formMetas = pgTable("form_metas", {
id: serial("id").primaryKey(),
@@ -94,16 +73,6 @@ export const formEntries = pgTable("form_entries", {
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
})
-export const formEntriesPlant = pgTable("form_entries_plant", {
- id: serial("id").primaryKey(),
- formCode: varchar("form_code", { length: 50 }).notNull(),
- data: jsonb("data").notNull(),
- projectCode: varchar("project_code", { length: 100 }).notNull(),
- packageCode: varchar("package_code", { length: 100 }).notNull(),
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
export const tags = pgTable("tags", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
contractItemId: integer("contract_item_id")
@@ -139,42 +108,6 @@ export const tags = pgTable("tags", {
};
});
-
-export const tagsPlant = pgTable("tags_plant", {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
- // SEDP에서 오는 고유 식별자
- tagIdx: varchar("tag_idx", { length: 100 }).notNull(),
-
- // 사용자가 편집 가능한 태그 번호
- tagNo: varchar("tag_no", { length: 100 }).notNull(),
-
- tagType: varchar("tag_type", { length: 50 }).notNull(),
- class: varchar("class", { length: 100 }).notNull(),
- tagClassId: integer("tag_class_id")
- .references(() => tagClasses.id, { onDelete: "set null" }),
- description: text("description"),
-
- // 동적 속성을 저장할 JSONB 컬럼 추가
- attributes: jsonb("attributes").$type<Record<string, string>>(),
-
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
-
- projectCode: varchar("project_code", { length: 100 }).notNull(),
- packageCode: varchar("package_code", { length: 100 }).notNull(),
- formId: integer("form_id"),
-
-}, (table) => {
- return {
- projectPackageTagIdxUnique: uniqueIndex("project_package_tag_idx_unique").on(
- table.projectCode,
- table.packageCode,
- table.tagIdx
- ),
- }
-})
-
// tagTypes에 projectId 추가 및 복합 기본키 생성
export const tagTypes = pgTable("tag_types", {
code: varchar("code", { length: 50 }).notNull(),
@@ -401,26 +334,6 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
-export const vendorDataReportTempsPlant = pgTable("vendor_data_report_temps_plant", {
- id: serial("id").primaryKey(),
-
- projectCode: varchar("project_code", { length: 100 }).notNull(),
- packageCode: varchar("package_code", { length: 100 }).notNull(),
- formId: integer("form_id")
- .notNull()
- .references(() => forms.id, { onDelete: "cascade" }),
- fileName: varchar("file_name", { length: 255 }).notNull(),
- filePath: varchar("file_path", { length: 1024 }).notNull(),
- createdAt: timestamp("created_at", { withTimezone: true })
- .defaultNow()
- .notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true })
- .defaultNow()
- .notNull(),
-});
-
-export type VendorDataReportTempsPlant = typeof vendorDataReportTempsPlant.$inferSelect;
-
export const formListsView = pgView("form_lists_view").as((qb) => {
return qb
diff --git a/lib/forms-plant/services.ts b/lib/forms-plant/services.ts
index 7e1976e6..219f36e4 100644
--- a/lib/forms-plant/services.ts
+++ b/lib/forms-plant/services.ts
@@ -7,18 +7,18 @@ import fs from "fs/promises";
import { v4 as uuidv4 } from "uuid";
import db from "@/db/db";
import {
- formEntries,formEntriesPlant,
- formMetas,formsPlant,
+ formEntries,
+ formMetas,
forms,
tagClassAttributes,
tagClasses,
- tags,tagsPlant,
+ tags,
tagSubfieldOptions,
tagSubfields,
tagTypeClassFormMappings,
tagTypes,
- vendorDataReportTempsPlant,
- VendorDataReportTempsPlant,
+ vendorDataReportTemps,
+ VendorDataReportTemps,
} from "@/db/schema/vendorData";
import { eq, and, desc, sql, DrizzleError, inArray, or, type SQL, type InferSelectModel } from "drizzle-orm";
import { unstable_cache } from "next/cache";
@@ -29,7 +29,6 @@ import { contractItems, contracts, items, projects } from "@/db/schema";
import { getSEDPToken } from "../sedp/sedp-token";
import { decryptWithServerAction } from "@/components/drm/drmUtils";
import { deleteFile, saveFile } from "@/lib/file-stroage";
-import { Register } from "@/components/form-data-plant/form-data-table-columns";
export type FormInfo = InferSelectModel<typeof forms>;
@@ -165,8 +164,7 @@ export interface EditableFieldsInfo {
// TAG별 편집 가능 필드 조회 함수
export async function getEditableFieldsByTag(
- projectCode: string,
- packageCode: string,
+ contractItemId: number,
projectId: number
): Promise<Map<string, string[]>> {
try {
@@ -176,11 +174,8 @@ export async function getEditableFieldsByTag(
tagNo: tags.tagNo,
tagClass: tags.class
})
- .from(tagsPlant)
- .where(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.packageCode, packageCode),
- );
+ .from(tags)
+ .where(eq(tags.contractItemId, contractItemId));
const editableFieldsMap = new Map<string, string[]>();
@@ -233,17 +228,26 @@ export async function getEditableFieldsByTag(
* data가 배열이면 그 배열을 반환,
* 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱.
*/
-export async function getFormData(formCode: string, projectCode: string, packageCode:string) {
+export async function getFormData(formCode: string, contractItemId: number) {
try {
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ // 기존 로직으로 projectId, columns, data 가져오기
+ const contractItemResult = await db
+ .select({
+ projectId: projects.id
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .innerJoin(projects, eq(contracts.projectId, projects.id))
+ .where(eq(contractItems.id, contractItemId))
+ .limit(1);
+
+ if (contractItemResult.length === 0) {
+ console.warn(`[getFormData] No contract item found with ID: ${contractItemId}`);
+ return { columns: null, data: [], editableFieldsMap: new Map() };
+ }
- const projectId = project.id;
+ const projectId = contractItemResult[0].projectId;
const metaRows = await db
.select()
@@ -265,15 +269,14 @@ export async function getFormData(formCode: string, projectCode: string, package
const entryRows = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
- .orderBy(desc(formEntriesPlant.updatedAt))
+ .orderBy(desc(formEntries.updatedAt))
.limit(1);
const entry = entryRows[0] ?? null;
@@ -318,7 +321,7 @@ export async function getFormData(formCode: string, projectCode: string, package
}
// *** 새로 추가: 편집 가능 필드 정보 계산 ***
- const editableFieldsMap = await getEditableFieldsByTag(projectCode,packageCode ,projectId);
+ const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId);
return { columns, data, editableFieldsMap };
@@ -328,16 +331,24 @@ export async function getFormData(formCode: string, projectCode: string, package
// Fallback logic (기존과 동일하게 editableFieldsMap 추가)
try {
- console.log(`[getFormData] Fallback DB query for (${formCode}, ${packageCode})`);
+ console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`);
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ const contractItemResult = await db
+ .select({
+ projectId: projects.id
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .innerJoin(projects, eq(contracts.projectId, projects.id))
+ .where(eq(contractItems.id, contractItemId))
+ .limit(1);
+
+ if (contractItemResult.length === 0) {
+ console.warn(`[getFormData] Fallback: No contract item found with ID: ${contractItemId}`);
+ return { columns: null, data: [], editableFieldsMap: new Map() };
+ }
- const projectId = project.id;
+ const projectId = contractItemResult[0].projectId;
const metaRows = await db
.select()
@@ -359,15 +370,14 @@ export async function getFormData(formCode: string, projectCode: string, package
const entryRows = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
- .orderBy(desc(formEntriesPlant.updatedAt))
+ .orderBy(desc(formEntries.updatedAt))
.limit(1);
const entry = entryRows[0] ?? null;
@@ -396,7 +406,7 @@ export async function getFormData(formCode: string, projectCode: string, package
}
// Fallback에서도 편집 가능 필드 정보 계산
- const editableFieldsMap = await getEditableFieldsByTag(projectCode, packageCode, projectId);
+ const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId);
return { columns, data, projectId, editableFieldsMap };
} catch (dbError) {
@@ -405,7 +415,7 @@ export async function getFormData(formCode: string, projectCode: string, package
}
}
}
-/**
+/**1
* contractId와 formCode(itemCode)를 사용하여 contractItemId를 찾는 서버 액션
*
* @param contractId - 계약 ID
@@ -507,26 +517,24 @@ export async function getPackageCodeById(contractItemId: number): Promise<string
export async function syncMissingTags(
- projectCode: string,
- packageCode: string,
+ contractItemId: number,
formCode: string
) {
// (1) Ensure there's a row in `forms` matching (contractItemId, formCode).
const [formRow] = await db
.select()
- .from(formsPlant)
+ .from(forms)
.where(
and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formCode)
+ eq(forms.contractItemId, contractItemId),
+ eq(forms.formCode, formCode)
)
)
.limit(1);
if (!formRow) {
throw new Error(
- `Form not found for projectCode=${projectCode}, formCode=${formCode}`
+ `Form not found for contractItemId=${contractItemId}, formCode=${formCode}`
);
}
@@ -550,28 +558,26 @@ export async function syncMissingTags(
// (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs.
const tagRows = await db
.select()
- .from(tagsPlant)
- .where(and(eq(tagsPlant.packageCode, packageCode),eq(tagsPlant.projectCode, projectCode), or(...orConditions)));
+ .from(tags)
+ .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions)));
// (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode).
let [entry] = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.packageCode, packageCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.formCode, formCode)
+ eq(formEntries.contractItemId, contractItemId),
+ eq(formEntries.formCode, formCode)
)
)
.limit(1);
if (!entry) {
const [inserted] = await db
- .insert(formEntriesPlant)
+ .insert(formEntries)
.values({
- projectCode,
- packageCode,
+ contractItemId,
formCode,
data: [], // Initialize with empty array
})
@@ -640,13 +646,13 @@ export async function syncMissingTags(
// (6) 실제로 추가되거나 수정되거나 삭제된 게 있다면 DB에 반영
if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) {
await db
- .update(formEntriesPlant)
+ .update(formEntries)
.set({ data: updatedData })
- .where(eq(formEntriesPlant.id, entry.id));
+ .where(eq(formEntries.id, entry.id));
}
// 캐시 무효화 등 후처리
- // revalidateTag(`form-data-${formCode}-${projectCode}`);
+ revalidateTag(`form-data-${formCode}-${contractItemId}`);
return { createdCount, updatedCount, deletedCount };
}
@@ -675,8 +681,7 @@ export interface UpdateResponse {
export async function updateFormDataInDB(
formCode: string,
- projectCode: string,
- packageCode: string,
+ contractItemId: number,
newData: Record<string, any>
): Promise<UpdateResponse> {
try {
@@ -692,12 +697,11 @@ export async function updateFormDataInDB(
// 2) row 찾기 (단 하나)
const entries = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
eq(formEntries.formCode, formCode),
- eq(formEntries.projectCode, projectCode),
- eq(formEntries.packageCode, packageCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
.limit(1);
@@ -752,12 +756,12 @@ export async function updateFormDataInDB(
// 6) DB UPDATE
try {
await db
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedArray,
updatedAt: new Date(), // 업데이트 시간도 갱신
})
- .where(eq(formEntriesPlant.id, entry.id));
+ .where(eq(formEntries.id, entry.id));
} catch (dbError) {
console.error("Database update error:", dbError);
@@ -777,7 +781,7 @@ export async function updateFormDataInDB(
// 7) Cache 무효화
try {
// 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정
- const cacheTag = `form-data-${formCode}-${projectCode}`;
+ const cacheTag = `form-data-${formCode}-${contractItemId}`;
console.log(cacheTag, "update")
revalidateTag(cacheTag);
} catch (cacheError) {
@@ -810,8 +814,7 @@ export async function updateFormDataInDB(
export async function updateFormDataBatchInDB(
formCode: string,
- projectCode: string,
- packageCode: string,
+ contractItemId: number,
newDataArray: Record<string, any>[]
): Promise<UpdateResponse> {
try {
@@ -836,12 +839,11 @@ export async function updateFormDataBatchInDB(
// 1) DB에서 현재 데이터 가져오기
const entries = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
.limit(1);
@@ -849,7 +851,7 @@ export async function updateFormDataBatchInDB(
if (!entries || entries.length === 0) {
return {
success: false,
- message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, projectCode=${projectCode})`,
+ message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`,
};
}
@@ -916,12 +918,12 @@ export async function updateFormDataBatchInDB(
// 3) DB에 한 번만 저장
try {
await db
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedArray,
updatedAt: new Date(),
})
- .where(eq(formEntriesPlant.id, entry.id));
+ .where(eq(formEntries.id, entry.id));
} catch (dbError) {
console.error("Database update error:", dbError);
@@ -950,7 +952,7 @@ export async function updateFormDataBatchInDB(
// 4) 캐시 무효화
try {
- const cacheTag = `form-data-${formCode}-${projectCode}`;
+ const cacheTag = `form-data-${formCode}-${contractItemId}`;
console.log(`Cache invalidated: ${cacheTag}`);
revalidateTag(cacheTag);
} catch (cacheError) {
@@ -1041,37 +1043,26 @@ export async function fetchFormMetadata(
}
type GetReportFileList = (
- projectCode: string,
- packageCode: string,
- formCode: string,
- mode: string
+ packageId: string,
+ formCode: string
) => Promise<{
formId: number;
}>;
-export const getFormId: GetReportFileList = async (projectCode, packageCode, formCode, mode) => {
+export const getFormId: GetReportFileList = async (packageId, formCode) => {
const result: { formId: number } = {
formId: 0,
};
try {
- // mode에 따른 조건 배열 생성
- const conditions = [
- eq(formsPlant.formCode, formCode),
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- ];
-
- // mode에 따라 추가 조건 설정
- if (mode === "IM") {
- conditions.push(eq(formsPlant.im, true));
- } else if (mode === "ENG") {
- conditions.push(eq(formsPlant.eng, true));
- }
-
const [targetForm] = await db
.select()
- .from(formsPlant)
- .where(and(...conditions));
+ .from(forms)
+ .where(
+ and(
+ eq(forms.formCode, formCode),
+ eq(forms.contractItemId, Number(packageId))
+ )
+ );
if (!targetForm) {
throw new Error("Not Found Target Form");
@@ -1081,34 +1072,30 @@ export const getFormId: GetReportFileList = async (projectCode, packageCode, for
result.formId = formId;
} catch (err) {
- console.error("Error getting form ID:", err);
} finally {
return result;
}
};
type getReportTempList = (
- projectCode: string,
- packageCode: string,
+ packageId: number,
formId: number
-) => Promise<VendorDataReportTempsPlant[]>;
+) => Promise<VendorDataReportTemps[]>;
export const getReportTempList: getReportTempList = async (
- projectCode,
- packageCode,
+ packageId,
formId
) => {
- let result: VendorDataReportTempsPlant[] = [];
+ let result: VendorDataReportTemps[] = [];
try {
result = await db
.select()
- .from(vendorDataReportTempsPlant)
+ .from(vendorDataReportTemps)
.where(
and(
- eq(vendorDataReportTempsPlant.projectCode, projectCode),
- eq(vendorDataReportTempsPlant.packageCode, packageCode),
- eq(vendorDataReportTempsPlant.formId, formId)
+ eq(vendorDataReportTemps.contractItemId, packageId),
+ eq(vendorDataReportTemps.formId, formId)
)
);
} catch (err) {
@@ -1118,8 +1105,7 @@ export const getReportTempList: getReportTempList = async (
};
export async function uploadReportTemp(
- projectCode: string,
- packageCode: string,
+ packageId: number,
formId: number,
formData: FormData
) {
@@ -1142,10 +1128,9 @@ export async function uploadReportTemp(
return db.transaction(async (tx) => {
// 파일 정보를 테이블에 저장
await tx
- .insert(vendorDataReportTempsPlant)
+ .insert(vendorDataReportTemps)
.values({
- projectCode,
- packageCode,
+ contractItemId: packageId,
formId: formId,
fileName: customFileName,
filePath: saveResult.publicPath!,
@@ -1175,16 +1160,16 @@ export const deleteReportTempFile: deleteReportTempFile = async (id) => {
return db.transaction(async (tx) => {
const [targetTempFile] = await tx
.select()
- .from(vendorDataReportTempsPlant)
- .where(eq(vendorDataReportTempsPlant.id, id));
+ .from(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
if (!targetTempFile) {
throw new Error("해당 Template File을 찾을 수 없습니다.");
}
await tx
- .delete(vendorDataReportTempsPlant)
- .where(eq(vendorDataReportTempsPlant.id, id));
+ .delete(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
const { filePath } = targetTempFile;
@@ -1325,8 +1310,7 @@ async function transformDataToSEDPFormat(
formCode: string,
objectCode: string,
projectNo: string,
- packageCode: string,
- contractItemId: string,
+ contractItemId: number, // Add contractItemId parameter
designerNo: string = "253213"
): Promise<SEDPDataItem[]> {
// Create a map for quick column lookup
@@ -1347,6 +1331,9 @@ async function transformDataToSEDPFormat(
// Cache for UOM factors to avoid duplicate API calls
const uomFactorCache = new Map<string, number>();
+ // Cache for packageCode to avoid duplicate DB queries for same tag
+ const packageCodeCache = new Map<string, string>();
+
// Cache for tagClass code to avoid duplicate DB queries for same tag
const tagClassCodeCache = new Map<string, string>();
@@ -1354,16 +1341,98 @@ async function transformDataToSEDPFormat(
const transformedItems = [];
for (const row of tableData) {
- let tagClassCode = "";
- // Get tagClass code if TAG_NO exists
+ const cotractItem = await db.query.contractItems.findFirst({
+ where:
+ eq(contractItems.id, contractItemId),
+ });
+
+ const item = await db.query.items.findFirst({
+ where:
+ eq(items.id, cotractItem.itemId),
+ });
+
+ // Get packageCode for this specific tag
+ let packageCode = item.packageCode; // fallback to formCode
+ let tagClassCode = ""; // for CLS_ID
+
if (row.TAG_NO && contractItemId) {
+ // Check cache first
const cacheKey = `${contractItemId}-${row.TAG_NO}`;
- if (tagClassCodeCache.has(cacheKey)) {
- tagClassCode = tagClassCodeCache.get(cacheKey)!;
+ if (packageCodeCache.has(cacheKey)) {
+ packageCode = packageCodeCache.get(cacheKey)!;
} else {
try {
+ // Query to get packageCode for this specific tag
+ const tagResult = await db.query.tags.findFirst({
+ where: and(
+ eq(tags.contractItemId, contractItemId),
+ eq(tags.tagNo, row.TAG_NO)
+ )
+ });
+
+ if (tagResult) {
+ // Get tagClass code if tagClassId exists
+ if (tagResult.tagClassId) {
+ // Check tagClass cache first
+ if (tagClassCodeCache.has(cacheKey)) {
+ tagClassCode = tagClassCodeCache.get(cacheKey)!;
+ } else {
+ const tagClassResult = await db.query.tagClasses.findFirst({
+ where: eq(tagClasses.id, tagResult.tagClassId)
+ });
+
+ if (tagClassResult) {
+ tagClassCode = tagClassResult.code;
+ console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`);
+ } else {
+ console.warn(`No tagClass found for tagClassId: ${tagResult.tagClassId}`);
+ }
+
+ // Cache the tagClass code result
+ tagClassCodeCache.set(cacheKey, tagClassCode);
+ }
+ }
+
+ // Get the contract item
+ const contractItemResult = await db.query.contractItems.findFirst({
+ where: eq(contractItems.id, tagResult.contractItemId)
+ });
+
+ if (contractItemResult) {
+ // Get the first item with this itemId
+ const itemResult = await db.query.items.findFirst({
+ where: eq(items.id, contractItemResult.itemId)
+ });
+
+ if (itemResult && itemResult.packageCode) {
+ packageCode = itemResult.packageCode;
+ console.log(`Found packageCode for tag ${row.TAG_NO}: ${packageCode}`);
+ } else {
+ console.warn(`No item found for contractItem.itemId: ${contractItemResult.itemId}, using fallback`);
+ }
+ } else {
+ console.warn(`No contractItem found for tag ${row.TAG_NO}, using fallback`);
+ }
+ } else {
+ console.warn(`No tag found for contractItemId: ${contractItemId}, tagNo: ${row.TAG_NO}, using fallback`);
+ }
+
+ // Cache the result (even if it's the fallback value)
+ packageCodeCache.set(cacheKey, packageCode);
+ } catch (error) {
+ console.error(`Error fetching packageCode for tag ${row.TAG_NO}:`, error);
+ // Use fallback value and cache it
+ packageCodeCache.set(cacheKey, packageCode);
+ }
+ }
+
+ // Get tagClass code if not already retrieved above
+ if (!tagClassCode && tagClassCodeCache.has(cacheKey)) {
+ tagClassCode = tagClassCodeCache.get(cacheKey)!;
+ } else if (!tagClassCode) {
+ try {
const tagResult = await db.query.tags.findFirst({
where: and(
eq(tags.contractItemId, contractItemId),
@@ -1371,20 +1440,22 @@ async function transformDataToSEDPFormat(
)
});
- if (tagResult?.tagClassId) {
+ if (tagResult && tagResult.tagClassId) {
const tagClassResult = await db.query.tagClasses.findFirst({
where: eq(tagClasses.id, tagResult.tagClassId)
});
-
+
if (tagClassResult) {
tagClassCode = tagClassResult.code;
console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`);
}
}
+ // Cache the tagClass code result
tagClassCodeCache.set(cacheKey, tagClassCode);
} catch (error) {
- console.error(`Error fetching tagClass for tag ${row.TAG_NO}:`, error);
+ console.error(`Error fetching tagClass code for tag ${row.TAG_NO}:`, error);
+ // Cache empty string as fallback
tagClassCodeCache.set(cacheKey, "");
}
}
@@ -1395,16 +1466,17 @@ async function transformDataToSEDPFormat(
TAG_NO: row.TAG_NO || "",
TAG_DESC: row.TAG_DESC || "",
ATTRIBUTES: [],
+ // SCOPE: objectCode,
SCOPE: packageCode,
- TOOLID: "eVCP",
+ TOOLID: "eVCP", // Changed from VDCS
ITM_NO: row.TAG_NO || "",
- OP_DELETE: row.status === "Deleted",
+ OP_DELETE: row.status === "Deleted", // Set OP_DELETE based on status
MAIN_YN: true,
LAST_REV_YN: true,
CRTER_NO: designerNo,
CHGER_NO: designerNo,
- TYPE: formCode,
- CLS_ID: tagClassCode,
+ TYPE: formCode, // Use packageCode instead of formCode
+ CLS_ID: tagClassCode, // Add CLS_ID with tagClass code
PROJ_NO: projectNo,
REV_NO: "00",
CRTE_DTM: currentTimestamp,
@@ -1450,7 +1522,7 @@ async function transformDataToSEDPFormat(
const uomData = await response.json();
if (uomData && uomData.FACTOR !== undefined && uomData.FACTOR !== null) {
factor = Number(uomData.FACTOR);
- // Store in cache for future use
+ // Store in cache for future use (type assertion to ensure it's a number)
uomFactorCache.set(column.uomId, factor);
}
} else {
@@ -1461,7 +1533,7 @@ async function transformDataToSEDPFormat(
}
}
- // Apply the factor if needed (currently commented out)
+ // Apply the factor if we got one
// if (factor !== undefined && typeof value === 'number') {
// value = value * factor;
// }
@@ -1469,7 +1541,7 @@ async function transformDataToSEDPFormat(
const attribute: SEDPAttribute = {
NAME: key,
- VALUE: String(value),
+ VALUE: String(value), // 모든 값을 문자열로 변환
UOM: column?.uom || "",
CLS_ID: tagClassCode || "",
};
@@ -1497,7 +1569,7 @@ export async function transformFormDataToSEDP(
formCode: string,
objectCode: string,
projectNo: string,
- packageCode: string, // Add contractItemId parameter
+ contractItemId: number, // Add contractItemId parameter
designerNo: string = "253213"
): Promise<SEDPDataItem[]> {
return transformDataToSEDPFormat(
@@ -1506,7 +1578,7 @@ export async function transformFormDataToSEDP(
formCode,
objectCode,
projectNo,
- packageCode,
+ contractItemId, // Pass contractItemId
designerNo
);
}
@@ -1527,20 +1599,6 @@ export async function getProjectCodeById(projectId: number): Promise<string> {
return projectRecord[0].code;
}
-export async function getProjectIdByCode(projectCode: string): Promise<number> {
- const projectRecord = await db
- .select({ id: projects.id })
- .from(projects)
- .where(eq(projects.code, projectCode))
- .limit(1);
-
- if (!projectRecord || projectRecord.length === 0) {
- throw new Error(`Project not found with ID: ${projectId}`);
- }
-
- return projectRecord[0].id;
-}
-
export async function getProjectById(projectId: number): Promise<{ code: string; type: string; }> {
const projectRecord = await db
.select({ code: projects.code , type:projects.type})
@@ -1621,13 +1679,13 @@ export async function sendDataToSEDP(
export async function sendFormDataToSEDP(
formCode: string,
projectId: number,
- projectCode: string, // contractItemId 파라미터 추가
- packageCode: string, // contractItemId 파라미터 추가
+ contractItemId: number, // contractItemId 파라미터 추가
formData: GenericData[],
columns: DataTableColumnJSON[]
): Promise<{ success: boolean; message: string; data?: any }> {
try {
// 1. Get project code
+ const projectCode = await getProjectCodeById(projectId);
// 2. Get class mapping
const mappingsResult = await db.query.tagTypeClassFormMappings.findFirst({
@@ -1670,7 +1728,7 @@ export async function sendFormDataToSEDP(
formCode,
objectCode,
projectCode,
- packageCode // Add contractItemId parameter
+ contractItemId // Add contractItemId parameter
);
// 4. Send to SEDP API
@@ -1681,12 +1739,11 @@ export async function sendFormDataToSEDP(
// Get the current formEntries data
const entries = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
.limit(1);
@@ -1721,17 +1778,17 @@ export async function sendFormDataToSEDP(
// Update the database
await db
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedDataArray,
updatedAt: new Date()
})
- .where(eq(formEntriesPlant.id, entry.id));
+ .where(eq(formEntries.id, entry.id));
console.log(`Updated status for ${sentTagNumbers.size} tags to "Sent to S-EDP"`);
}
} else {
- console.warn(`No formEntriesPlant found for formCode: ${formCode}`);
+ console.warn(`No formEntries found for formCode: ${formCode}, contractItemId: ${contractItemId}`);
}
} catch (statusUpdateError) {
// Status 업데이트 실패는 경고로만 처리 (SEDP 전송은 성공했으므로)
@@ -1755,14 +1812,12 @@ export async function sendFormDataToSEDP(
export async function deleteFormDataByTags({
formCode,
- projectCode,
- packageCode,
+ contractItemId,
tagIdxs,
projectId,
}: {
formCode: string
- projectCode: string
- packageCode: string
+ contractItemId: number
tagIdxs: string[]
projectId?: number
}): Promise<{
@@ -1775,26 +1830,25 @@ export async function deleteFormDataByTags({
}> {
try {
// 입력 검증
- if (!formCode || !projectCode || !Array.isArray(tagIdxs) || tagIdxs.length === 0) {
+ if (!formCode || !contractItemId || !Array.isArray(tagIdxs) || tagIdxs.length === 0) {
return {
error: "Missing required parameters: formCode, contractItemId, tagIdxs",
}
}
- console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, projectCode: ${projectCode}, tagIdxs:`, tagIdxs)
+ console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagIdxs:`, tagIdxs)
// 1. 트랜잭션 전에 삭제할 항목들을 미리 조회하여 저장 (S-EDP 전송용)
const entryForSedp = await db
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
- .orderBy(desc(formEntriesPlant.updatedAt))
+ .orderBy(desc(formEntries.updatedAt))
.limit(1)
let itemsToSendToSedp: Record<string, unknown>[] = []
@@ -1814,15 +1868,14 @@ export async function deleteFormDataByTags({
// 2-1. 현재 formEntry 데이터 가져오기
const currentEntryResult = await tx
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
- .orderBy(desc(formEntriesPlant.updatedAt))
+ .orderBy(desc(formEntries.updatedAt))
.limit(1)
if (currentEntryResult.length === 0) {
@@ -1850,15 +1903,14 @@ export async function deleteFormDataByTags({
// 2-3. tags 테이블에서 해당 태그들 삭제
const deletedTagsResult = await tx
- .delete(tagsPlant)
+ .delete(tags)
.where(
and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.packageCode, packageCode),
- inArray(tagsPlant.tagIdx, tagIdxs)
+ eq(tags.contractItemId, contractItemId),
+ inArray(tags.tagIdx, tagIdxs)
)
)
- .returning({ tagNo: tagsPlant.tagNo })
+ .returning({ tagNo: tags.tagNo })
const deletedTagsCount = deletedTagsResult.length
@@ -1867,16 +1919,15 @@ export async function deleteFormDataByTags({
// 2-4. formEntries 데이터 업데이트 (삭제된 항목 제외)
await tx
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedData,
updatedAt: new Date(),
})
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
@@ -1969,8 +2020,7 @@ export async function deleteFormDataByTags({
const sedpResult = await sendFormDataToSEDP(
formCode,
projectId,
- projectCode,
- packageCode,
+ contractItemId,
uniqueDeletedItems as GenericData[],
formMetaResult.columns as DataTableColumnJSON[]
)
@@ -2029,13 +2079,11 @@ export async function deleteFormDataByTags({
*/
export async function excludeFormDataByTags({
formCode,
- projectCode,
- packageCode,
+ contractItemId,
tagNumbers,
}: {
formCode: string
- projectCode: string
- packageCode: string
+ contractItemId: number
tagNumbers: string[]
}): Promise<{
error?: string
@@ -2044,28 +2092,27 @@ export async function excludeFormDataByTags({
}> {
try {
// 입력 검증
- if (!formCode || !projectCode || !Array.isArray(tagNumbers) || tagNumbers.length === 0) {
+ if (!formCode || !contractItemId || !Array.isArray(tagNumbers) || tagNumbers.length === 0) {
return {
- error: "Missing required parameters: formCode, projectCode, tagNumbers",
+ error: "Missing required parameters: formCode, contractItemId, tagNumbers",
}
}
- console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, projectCode: ${projectCode}, tagNumbers:`, tagNumbers)
+ console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNumbers:`, tagNumbers)
// 트랜잭션으로 안전하게 처리
const result = await db.transaction(async (tx) => {
// 1. 현재 formEntry 데이터 가져오기
const currentEntryResult = await tx
.select()
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
- .orderBy(desc(formEntriesPlant.updatedAt))
+ .orderBy(desc(formEntries.updatedAt))
.limit(1)
if (currentEntryResult.length === 0) {
@@ -2099,16 +2146,15 @@ export async function excludeFormDataByTags({
// 3. formEntries 데이터 업데이트
await tx
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedData,
updatedAt: new Date(),
})
.where(
and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, contractItemId)
)
)
@@ -2119,7 +2165,7 @@ export async function excludeFormDataByTags({
})
// 4. 캐시 무효화
- const cacheKey = `form-data-${formCode}-${packageCode}`
+ const cacheKey = `form-data-${formCode}-${contractItemId}`
revalidateTag(cacheKey)
console.log(`[EXCLUDE ACTION] Transaction completed successfully`)
diff --git a/lib/forms-plant/stat.ts b/lib/forms-plant/stat.ts
index f734e782..f13bab61 100644
--- a/lib/forms-plant/stat.ts
+++ b/lib/forms-plant/stat.ts
@@ -1,7 +1,7 @@
"use server"
import db from "@/db/db"
-import { vendors, contracts, contractItems, forms,formsPlant,formEntriesPlant, formEntries, formMetas, tags,tagsPlant, tagClasses, tagClassAttributes, projects } from "@/db/schema"
+import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema"
import { eq, and, inArray } from "drizzle-orm"
import { getEditableFieldsByTag } from "./services"
import { getServerSession } from "next-auth/next"
@@ -218,7 +218,7 @@ export async function getVendorFormStatus(projectId?: number): Promise<VendorFor
-export async function getFormStatusByVendor(projectId: number, projectCode: string, packageCode: string, formCode: string): Promise<FormStatusByVendor[]> {
+export async function getFormStatusByVendor(projectId: number, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> {
try {
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
@@ -244,16 +244,15 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri
// 4. contractItem별 forms 조회
const formsList = await db
.select({
- id: formsPlant.id,
- formCode: formsPlant.formCode,
- contractItemId: formsPlant.contractItemId
+ id: forms.id,
+ formCode: forms.formCode,
+ contractItemId: forms.contractItemId
})
- .from(formsPlant)
+ .from(forms)
.where(
and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formCode)
+ eq(forms.contractItemId, contractItemId),
+ eq(forms.formCode, formCode)
)
)
@@ -262,21 +261,20 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri
// 5. formEntries 조회
const entriesList = await db
.select({
- id: formEntriesPlant.id,
- formCode: formEntriesPlant.formCode,
- data: formEntriesPlant.data
+ id: formEntries.id,
+ formCode: formEntries.formCode,
+ data: formEntries.data
})
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.packageCode, packageCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.formCode, formCode)
+ eq(formEntries.contractItemId, contractItemId),
+ eq(formEntries.formCode, formCode)
)
)
// 6. TAG별 편집 가능 필드 조회
- const editableFieldsByTag = await getEditableFieldsByTag(projectCode,packageCode, projectId)
+ const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId)
const vendorStatusList: VendorFormStatus[] = []
diff --git a/lib/sedp/get-form-tags-plant.ts b/lib/sedp/get-form-tags-plant.ts
deleted file mode 100644
index 176f1b3f..00000000
--- a/lib/sedp/get-form-tags-plant.ts
+++ /dev/null
@@ -1,933 +0,0 @@
-import db from "@/db/db";
-import {
- contractItems,
- tagsPlant,
- formsPlant,formEntriesPlant,
- items,
- tagTypeClassFormMappings,
- projects,
- tagTypes,
- tagClasses,
- formMetas,
-} from "@/db/schema";
-import { eq, and, like, inArray } from "drizzle-orm";
-import { getSEDPToken } from "./sedp-token";
-import { getFormMappingsByTagTypebyProeject } from "../tags/form-mapping-service";
-
-
-interface Attribute {
- ATT_ID: string;
- VALUE: any;
- VALUE_DBL: number;
- UOM_ID: string | null;
-}
-
-interface TagEntry {
- TAG_IDX: string;
- TAG_NO: string;
- BF_TAG_NO: string;
- TAG_DESC: string;
- EP_ID: string;
- TAG_TYPE_ID: string;
- CLS_ID: string;
- ATTRIBUTES: Attribute[];
- [key: string]: any;
-}
-
-interface Column {
- key: string;
- label: string;
- type: string;
- shi?: string | null;
-}
-
-interface newRegister {
- PROJ_NO: string;
- MAP_ID: string;
- EP_ID: string;
- CATEGORY: string;
- BYPASS: boolean;
- REG_TYPE_ID: string;
- TOOL_ID: string;
- TOOL_TYPE: string;
- SCOPES: string[];
- MAP_CLS: {
- TOOL_ATT_NAME: string;
- ITEMS: ClassItmes[];
- };
- MAP_ATT: MapAttribute[];
- MAP_TMPLS: string[];
- CRTER_NO: string;
- CRTE_DTM: string;
- CHGER_NO: string;
- _id: string;
-}
-
-interface ClassItmes {
- SEDP_OBJ_CLS_ID: string;
- TOOL_VALS: string;
- ISDEFALUT: boolean;
-}
-
-interface MapAttribute {
- SEDP_ATT_ID: string;
- TOOL_ATT_NAME: string;
- KEY_YN: boolean;
- DUE_DATE: string; //"YYYY-MM-DDTHH:mm:ssZ"
- INOUT: string | null;
-}
-
-
-
-async function getNewRegisters(projectCode: string): Promise<newRegister[]> {
- try {
- // 토큰(API 키) 가져오기
- const apiKey = await getSEDPToken();
-
- const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
-
- const response = await fetch(
- `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- "TOOL_ID": "eVCP"
- })
- }
- );
-
- if (!response.ok) {
- throw new Error(`새 레지스터 요청 실패: ${response.status} ${response.statusText}`);
- }
-
- // 안전하게 JSON 파싱
- let data;
- try {
- data = await response.json();
- } catch (parseError) {
- console.error(`프로젝트 ${projectCode}의 새 레지스터 응답 파싱 실패:`, parseError);
- // 응답 내용 로깅
- const text = await response.clone().text();
- console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
- throw new Error(`새 레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
- }
-
- // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑)
- let registers: newRegister[] = Array.isArray(data) ? data : [data];
-
- console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 새 레지스터를 가져왔습니다.`);
- return registers;
- } catch (error) {
- console.error(`프로젝트 ${projectCode}의 새 레지스터 가져오기 실패:`, error);
- throw error;
- }
-}
-
-
-/**
- * 태그 가져오기 서비스 함수
- * formEntries와 tags 테이블 모두에 데이터를 저장
- */
-export async function importTagsFromSEDP(
- formCode: string,
- projectCode: string,
- packageCode: string,
- progressCallback?: (progress: number) => void
-): Promise<{
- processedCount: number;
- excludedCount: number;
- totalEntries: number;
- formCreated?: boolean;
- errors?: string[];
-}> {
- try {
- // 진행 상황 보고
- if (progressCallback) progressCallback(5);
-
- // 에러 수집 배열
- const errors: string[] = [];
-
- // SEDP API에서 태그 데이터 가져오기
- const tagData = await fetchTagDataFromSEDP(projectCode, formCode);
- const newRegisters = await getNewRegisters(projectCode);
-
- const registerMatched = newRegisters.find(v => v.REG_TYPE_ID === formCode).MAP_ATT
-
-
- // 트랜잭션으로 모든 DB 작업 처리
- return await db.transaction(async (tx) => {
- // 프로젝트 정보 가져오기 (type 포함)
- const projectRecord = await tx.select({ id: projects.id, type: projects.type })
- .from(projects)
- .where(eq(projects.code, projectCode))
- .limit(1);
-
- if (!projectRecord || projectRecord.length === 0) {
- throw new Error(`Project not found for code: ${projectCode}`);
- }
-
- const projectId = projectRecord[0].id;
- const projectType = projectRecord[0].type;
-
- // 프로젝트 타입에 따라 packageCode를 찾을 ATT_ID 결정
- const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074";
-
-
-
-
- const targetPackageCode = packageCode;
-
- // 데이터 형식 처리 - tagData의 첫 번째 키 사용
- const tableName = Object.keys(tagData)[0];
-
- if (!tableName || !tagData[tableName]) {
- throw new Error("Invalid tag data format from SEDP API");
- }
-
- const allTagEntries: TagEntry[] = tagData[tableName];
-
- if (!Array.isArray(allTagEntries) || allTagEntries.length === 0) {
- return {
- processedCount: 0,
- excludedCount: 0,
- totalEntries: 0,
- errors: ["No tag entries found in API response"]
- };
- }
-
- // packageCode로 필터링 - ATTRIBUTES에서 지정된 ATT_ID의 VALUE와 packageCode 비교
- const tagEntries = allTagEntries.filter(entry => {
- if (Array.isArray(entry.ATTRIBUTES)) {
- const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId);
- if (packageCodeAttr && packageCodeAttr.VALUE === targetPackageCode) {
- return true;
- }
- }
- return false;
- });
-
- if (tagEntries.length === 0) {
- return {
- processedCount: 0,
- excludedCount: 0,
- totalEntries: allTagEntries.length,
- errors: [`No tag entries found with ${packageCodeAttId} attribute value matching packageCode: ${targetPackageCode}`]
- };
- }
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(20);
-
- // 나머지 코드는 기존과 동일...
- // form ID 가져오기 - 없으면 생성
- let formRecord = await tx.select({ id: formsPlant.id })
- .from(formsPlant)
- .where(and(
- eq(formsPlant.formCode, formCode),
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode)
- ))
- .limit(1);
-
- let formCreated = false;
-
- // form이 없으면 생성
- if (!formRecord || formRecord.length === 0) {
- console.log(`[IMPORT TAGS] Form ${formCode} not found, attempting to create...`);
-
- // 첫 번째 태그의 정보를 사용해서 form mapping을 찾습니다
- // 모든 태그가 같은 formCode를 사용한다고 가정
- if (tagEntries.length > 0) {
- const firstTag = tagEntries[0];
-
- // tagType 조회 (TAG_TYPE_ID -> description)
- let tagTypeDescription = firstTag.TAG_TYPE_ID; // 기본값
- if (firstTag.TAG_TYPE_ID) {
- const tagTypeRecord = await tx.select({ description: tagTypes.description })
- .from(tagTypes)
- .where(and(
- eq(tagTypes.code, firstTag.TAG_TYPE_ID),
- eq(tagTypes.projectId, projectId)
- ))
- .limit(1);
-
- if (tagTypeRecord && tagTypeRecord.length > 0) {
- tagTypeDescription = tagTypeRecord[0].description;
- }
- }
-
- // tagClass 조회 (CLS_ID -> label)
- let tagClassLabel = firstTag.CLS_ID; // 기본값
- if (firstTag.CLS_ID) {
- const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label })
- .from(tagClasses)
- .where(and(
- eq(tagClasses.code, firstTag.CLS_ID),
- eq(tagClasses.projectId, projectId)
- ))
- .limit(1);
-
- if (tagClassRecord && tagClassRecord.length > 0) {
- tagClassLabel = tagClassRecord[0].label;
- }
- }
-
- // 태그 타입에 따른 폼 정보 가져오기
- const allFormMappings = await getFormMappingsByTagTypebyProeject(
- projectId,
- );
-
- // 현재 formCode와 일치하는 매핑 찾기
- const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode);
-
- if (targetFormMapping) {
- console.log(`[IMPORT TAGS] Found form mapping for ${formCode}, creating form...`);
-
- // form 생성
- const insertResult = await tx
- .insert(formsPlant)
- .values({
- projectCode,
- packageCode,
- formCode: targetFormMapping.formCode,
- formName: targetFormMapping.formName,
- eng: true, // ENG 모드에서 가져오는 것이므로 eng: true
- im: targetFormMapping.ep === "IMEP" ? true : false
- })
- .returning({ id: formsPlant.id });
-
- formRecord = insertResult;
- formCreated = true;
-
- console.log(`[IMPORT TAGS] Successfully created form:`, insertResult[0]);
- } else {
- console.log(`[IMPORT TAGS] No form mapping found for formCode: ${formCode}`);
- console.log(`[IMPORT TAGS] Available mappings:`, allFormMappings.map(m => m.formCode));
- throw new Error(`Form ${formCode} not found and no mapping available for tag type ${tagTypeDescription}`);
- }
- } else {
- throw new Error(`Form not found for formCode: ${formCode} and, and no tags to derive form mapping`);
- }
- } else {
- console.log(`[IMPORT TAGS] Found existing form:`, formRecord[0].id);
-
- // 기존 form이 있는 경우 eng와 im 필드를 체크하고 업데이트
- const existingForm = await tx.select({
- eng: formsPlant.eng,
- im: formsPlant.im
- })
- .from(formsPlant)
- .where(eq(formsPlant.id, formRecord[0].id))
- .limit(1);
-
- if (existingForm.length > 0) {
- // form mapping 정보 가져오기 (im 필드 업데이트를 위해)
- let shouldUpdateIm = false;
- let targetImValue = false;
-
- // 첫 번째 태그의 정보를 사용해서 form mapping을 확인
- if (tagEntries.length > 0) {
- const firstTag = tagEntries[0];
-
- // tagType 조회
- let tagTypeDescription = firstTag.TAG_TYPE_ID;
- if (firstTag.TAG_TYPE_ID) {
- const tagTypeRecord = await tx.select({ description: tagTypes.description })
- .from(tagTypes)
- .where(and(
- eq(tagTypes.code, firstTag.TAG_TYPE_ID),
- eq(tagTypes.projectId, projectId)
- ))
- .limit(1);
-
- if (tagTypeRecord && tagTypeRecord.length > 0) {
- tagTypeDescription = tagTypeRecord[0].description;
- }
- }
-
- // tagClass 조회
- let tagClassLabel = firstTag.CLS_ID;
- if (firstTag.CLS_ID) {
- const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label })
- .from(tagClasses)
- .where(and(
- eq(tagClasses.code, firstTag.CLS_ID),
- eq(tagClasses.projectId, projectId)
- ))
- .limit(1);
-
- if (tagClassRecord && tagClassRecord.length > 0) {
- tagClassLabel = tagClassRecord[0].label;
- }
- }
-
- // form mapping 정보 가져오기
- const allFormMappings = await getFormMappingsByTagTypebyProeject(
- projectId,
- );
-
- // 현재 formCode와 일치하는 매핑 찾기
- const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode);
-
- if (targetFormMapping) {
- targetImValue = targetFormMapping.ep === "IMEP";
- shouldUpdateIm = existingForm[0].im !== targetImValue;
- }
- }
-
- // 업데이트할 필드들 준비
- const updates: any = {};
- let hasUpdates = false;
-
- // eng 필드 체크
- if (existingForm[0].eng !== true) {
- updates.eng = true;
- hasUpdates = true;
- }
-
- // im 필드 체크
- if (shouldUpdateIm) {
- updates.im = targetImValue;
- hasUpdates = true;
- }
-
- // 업데이트 실행
- if (hasUpdates) {
- await tx
- .update(formsPlant)
- .set(updates)
- .where(eq(formsPlant.id, formRecord[0].id));
-
- console.log(`[IMPORT TAGS] Form ${formRecord[0].id} updated with:`, updates);
- }
- }
- }
-
- const formId = formRecord[0].id;
-
- // 나머지 처리 로직은 기존과 동일...
- // (양식 메타데이터 가져오기, 태그 처리 등)
-
- // 양식 메타데이터 가져오기
- const formMetaRecord = await tx.select({ columns: formMetas.columns })
- .from(formMetas)
- .where(and(
- eq(formMetas.projectId, projectId),
- eq(formMetas.formCode, formCode)
- ))
- .limit(1);
-
- if (!formMetaRecord || formMetaRecord.length === 0) {
- throw new Error(`Form metadata not found for formCode: ${formCode} and projectId: ${projectId}`);
- }
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(30);
-
- // 컬럼 정보 파싱
- const columnsJSON: Column[] = (formMetaRecord[0].columns);
-
- // 현재 formEntries 데이터 가져오기
- const existingEntries = await tx.select({ id: formEntriesPlant.id, data: formEntriesPlant.data })
- .from(formEntriesPlant)
- .where(and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
- ));
-
- // 기존 tags 데이터 가져오기
- const existingTags = await tx.select()
- .from(tagsPlant)
- .where(and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.packageCode, packageCode),
- )
- );
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(50);
-
- // 기존 데이터를 맵으로 변환
- const existingTagMap = new Map();
- const existingTagsMap = new Map();
-
- existingEntries.forEach(entry => {
- const data = entry.data as any[];
- data.forEach(item => {
- if (item.TAG_IDX) {
- existingTagMap.set(item.TAG_IDX, {
- entryId: entry.id,
- data: item
- });
- }
- });
- });
-
- existingTags.forEach(tag => {
- existingTagsMap.set(tag.tagIdx, tag);
- });
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(60);
-
- // 처리 결과 카운터
- let processedCount = 0;
- let excludedCount = 0;
-
- // 새로운 태그 데이터와 업데이트할 데이터 준비
- const newTagData: any[] = [];
- const upsertTagRecords: any[] = []; // 새로 추가되거나 업데이트될 태그들
- const updateData: { entryId: number, tagNo: string, updates: any }[] = [];
-
- // SEDP 태그 데이터 처리
- for (const tagEntry of tagEntries) {
- try {
- if (!tagEntry.TAG_IDX) {
- excludedCount++;
- errors.push(`Missing TAG_NO in tag entry`);
- continue;
- }
-
- // tagType 조회 (TAG_TYPE_ID -> description)
- let tagTypeDescription = tagEntry.TAG_TYPE_ID; // 기본값
- if (tagEntry.TAG_TYPE_ID) {
- const tagTypeRecord = await tx.select({ description: tagTypes.description })
- .from(tagTypes)
- .where(and(
- eq(tagTypes.code, tagEntry.TAG_TYPE_ID),
- eq(tagTypes.projectId, projectId)
- ))
- .limit(1);
-
- if (tagTypeRecord && tagTypeRecord.length > 0) {
- tagTypeDescription = tagTypeRecord[0].description;
- }
- }
-
- // tagClass 조회 (CLS_ID -> label)
- let tagClassLabel = tagEntry.CLS_ID; // 기본값
- let tagClassId = null; // 기본값
- if (tagEntry.CLS_ID) {
- const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label })
- .from(tagClasses)
- .where(and(
- eq(tagClasses.code, tagEntry.CLS_ID),
- eq(tagClasses.projectId, projectId)
- ))
- .limit(1);
-
- if (tagClassRecord && tagClassRecord.length > 0) {
- tagClassLabel = tagClassRecord[0].label;
- tagClassId = tagClassRecord[0].id;
- }
- }
-
- const packageCode = projectType === "ship" ? tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "CM3003")?.VALUE : tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "ME5074")?.VALUE
-
- // 기본 태그 데이터 객체 생성 (formEntries용)
- const tagObject: any = {
- TAG_IDX: tagEntry.TAG_IDX, // SEDP 고유 식별자
- TAG_NO: tagEntry.TAG_NO,
- TAG_DESC: tagEntry.TAG_DESC || "",
- CLS_ID: tagEntry.CLS_ID || "",
- VNDRCD: vendorRecord[0].vendorCode,
- VNDRNM_1: vendorRecord[0].vendorName,
- status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시
- source: "S-EDP", // 태그 출처 (불변) - S-EDP에서 가져옴
- ...(projectType === "ship" ? { CM3003: packageCode } : { ME5074: packageCode })
- }
-
- let latestDueDate: Date | null = null;
-
- // tags 테이블용 데이터 (UPSERT용)
- const tagRecord = {
- projectCode,
- packageCode,
- formId: formId,
- tagIdx: tagEntry.TAG_IDX, // SEDP 고유 식별자
- tagNo: tagEntry.TAG_NO,
- tagType: tagTypeDescription || "",
- class: tagClassLabel,
- tagClassId: tagClassId,
- description: tagEntry.TAG_DESC || null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
-
- // ATTRIBUTES 필드에서 shi=true인 컬럼의 값 추출
- if (Array.isArray(tagEntry.ATTRIBUTES)) {
- for (const attr of tagEntry.ATTRIBUTES) {
- const columnInfo = columnsJSON.find(col => col.key === attr.ATT_ID);
- if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) {
- if (columnInfo.type === "NUMBER") {
- if (attr.VALUE !== undefined && attr.VALUE !== null) {
- if (typeof attr.VALUE === 'string') {
- const numberMatch = attr.VALUE.match(/(-?\d+(\.\d+)?)/);
- if (numberMatch) {
- tagObject[attr.ATT_ID] = parseFloat(numberMatch[0]);
- } else {
- const parsed = parseFloat(attr.VALUE);
- if (!isNaN(parsed)) {
- tagObject[attr.ATT_ID] = parsed;
- }
- }
- } else if (typeof attr.VALUE === 'number') {
- tagObject[attr.ATT_ID] = attr.VALUE;
- }
- }
- } else if (attr.VALUE !== null && attr.VALUE !== undefined) {
- tagObject[attr.ATT_ID] = attr.VALUE;
- }
- }
-
- // registerMatched에서 해당 SEDP_ATT_ID의 DUE_DATE 찾기
- if (registerMatched && Array.isArray(registerMatched)) {
- const matchedAttribute = registerMatched.find(
- regAttr => regAttr.SEDP_ATT_ID === attr.ATT_ID
- );
-
- if (matchedAttribute && matchedAttribute.DUE_DATE) {
- try {
- const dueDate = new Date(matchedAttribute.DUE_DATE);
-
- // 유효한 날짜인지 확인
- if (!isNaN(dueDate.getTime())) {
- // 첫 번째 DUE_DATE이거나 현재까지 찾은 것보다 더 늦은 날짜인 경우 업데이트
- if (!latestDueDate || dueDate > latestDueDate) {
- latestDueDate = dueDate;
- }
- }
- } catch (dateError) {
- console.warn(`Invalid DUE_DATE format for ${attr.ATT_ID}: ${matchedAttribute.DUE_DATE}`);
- }
- }
- }
-
- }
- }
-
- if (latestDueDate) {
- // ISO 형식의 문자열로 저장 (또는 원하는 형식으로 변경 가능)
- tagObject.DUE_DATE = latestDueDate.toISOString();
-
- // 또는 YYYY-MM-DD 형식을 원한다면:
- // tagObject.DUE_DATE = latestDueDate.toISOString().split('T')[0];
-
- // 또는 원본 형식 그대로 유지하려면:
- // tagObject.DUE_DATE = latestDueDate.toISOString().replace('Z', '');
- }
-
-
-
- // 기존 태그가 있는지 확인하고 처리
- const existingTag = existingTagMap.get(tagEntry.TAG_IDX);
-
- if (existingTag) {
- // 기존 태그가 있으면 formEntries 업데이트 데이터 준비
- const updates: any = {};
- let hasUpdates = false;
-
- for (const key of Object.keys(tagObject)) {
- if (key === "TAG_IDX") continue;
-
- if (key === "TAG_NO" && tagObject[key] !== existingTag.data[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- continue;
- }
-
-
- if (key === "TAG_DESC" && tagObject[key] !== existingTag.data[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- continue;
- }
-
- if (key === "status" && tagObject[key] !== existingTag.data[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- continue;
- }
- if (key === "CLS_ID" && tagObject[key] !== existingTag.data[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- continue;
- }
-
- if (key === "DUE_DATE" && tagObject[key] !== existingTag.data[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- continue;
- }
-
- const columnInfo = columnsJSON.find(col => col.key === key);
- if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) {
- if (existingTag.data[key] !== tagObject[key]) {
- updates[key] = tagObject[key];
- hasUpdates = true;
- }
- }
- }
-
- if (hasUpdates) {
- updateData.push({
- entryId: existingTag.entryId,
- tagIdx: tagEntry.TAG_IDX, // TAG_IDX로 변경
- updates
- });
- }
- } else {
- // 기존 태그가 없으면 새로 추가
- newTagData.push(tagObject);
- }
-
- // tags 테이블에는 항상 upsert (새로 추가되거나 업데이트)
- upsertTagRecords.push(tagRecord);
-
- processedCount++;
- } catch (error) {
- excludedCount++;
- errors.push(`Error processing tag ${tagEntry.TAG_IDX || 'unknown'}: ${error}`);
- }
- }
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(80);
-
- // formEntries 업데이트 실행
- // entryId별로 업데이트를 그룹화
- const updatesByEntryId = new Map();
-
- for (const update of updateData) {
- if (!updatesByEntryId.has(update.entryId)) {
- updatesByEntryId.set(update.entryId, []);
- }
- updatesByEntryId.get(update.entryId).push(update);
- }
-
- // 그룹화된 업데이트를 처리
- for (const [entryId, updates] of updatesByEntryId) {
- try {
- const entry = existingEntries.find(e => e.id === entryId);
- if (!entry) continue;
-
- const data = entry.data as any[];
-
- // 해당 entryId의 모든 업데이트를 한 번에 적용
- const updatedData = data.map(item => {
- let updatedItem = { ...item };
-
- // 현재 item에 적용할 모든 업데이트를 찾아서 적용
- for (const update of updates) {
- if (item.TAG_IDX === update.tagIdx) {
- updatedItem = { ...updatedItem, ...update.updates };
- }
- }
-
- return updatedItem;
- });
-
- // entryId별로 한 번만 DB 업데이트
- await tx.update(formEntriesPlant)
- .set({
- data: updatedData,
- updatedAt: new Date()
- })
- .where(eq(formEntriesPlant.id, entryId));
-
- } catch (error) {
- const tagNos = updates.map(u => u.tagNo || u.tagIdx).join(', ');
- errors.push(`Error updating formEntry ${entryId} for tags ${tagNos}: ${error}`);
- }
- }
-
- // 새 태그 추가 (formEntriesPlant)
- if (newTagData.length > 0) {
- if (existingEntries.length > 0) {
- const firstEntry = existingEntries[0];
- const existingData = firstEntry.data as any[];
- const updatedData = [...existingData, ...newTagData];
-
- await tx.update(formEntriesPlant)
- .set({
- data: updatedData,
- updatedAt: new Date()
- })
- .where(eq(formEntriesPlant.id, firstEntry.id));
- } else {
- await tx.insert(formEntriesPlant)
- .values({
- formCode,
- projectCode,
- packageCode,
- data: newTagData,
- createdAt: new Date(),
- updatedAt: new Date()
- });
- }
- }
-
- // tags 테이블 처리 (INSERT + UPDATE 분리)
- if (upsertTagRecords.length > 0) {
- const newTagRecords: any[] = [];
- const updateTagRecords: { tagId: number, updates: any }[] = [];
-
- // 각 태그를 확인하여 신규/업데이트 분류
- for (const tagRecord of upsertTagRecords) {
- const existingTagRecord = existingTagsMap.get(tagRecord.tagIdx);
-
- if (existingTagRecord) {
- // 기존 태그가 있으면 업데이트 준비
- const tagUpdates: any = {};
- let hasTagUpdates = false;
-
- // tagNo도 업데이트 가능 (편집된 경우)
- if (existingTagRecord.tagNo !== tagRecord.tagNo) {
- tagUpdates.tagNo = tagRecord.tagNo;
- hasTagUpdates = true;
- }
-
- if (existingTagRecord.tagType !== tagRecord.tagType) {
- tagUpdates.tagType = tagRecord.tagType;
- hasTagUpdates = true;
- }
- if (existingTagRecord.class !== tagRecord.class) {
- tagUpdates.class = tagRecord.class;
- hasTagUpdates = true;
- }
- if (existingTagRecord.tagClassId !== tagRecord.tagClassId) {
- tagUpdates.tagClassId = tagRecord.tagClassId;
- hasTagUpdates = true;
- }
-
- if (existingTagRecord.description !== tagRecord.description) {
- tagUpdates.description = tagRecord.description;
- hasTagUpdates = true;
- }
- if (existingTagRecord.formId !== tagRecord.formId) {
- tagUpdates.formId = tagRecord.formId;
- hasTagUpdates = true;
- }
-
- if (hasTagUpdates) {
- updateTagRecords.push({
- tagId: existingTagRecord.id,
- updates: { ...tagUpdates, updatedAt: new Date() }
- });
- }
- } else {
- // 새로운 태그
- newTagRecords.push(tagRecord);
- }
- }
-
- // 새 태그 삽입
- if (newTagRecords.length > 0) {
- try {
- await tx.insert(tagsPlant)
- .values(newTagRecords)
- .onConflictDoNothing({
- target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx]
- });
- } catch (error) {
- // 개별 삽입으로 재시도
- for (const tagRecord of newTagRecords) {
- try {
- await tx.insert(tagsPlant)
- .values(tagRecord)
- .onConflictDoNothing({
- target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx]
- });
- } catch (individualError) {
- errors.push(`Error inserting tag ${tagRecord.tagIdx}: ${individualError}`);
- }
- }
- }
- }
-
- // 기존 태그 업데이트
- for (const update of updateTagRecords) {
- try {
- await tx.update(tagsPlant)
- .set(update.updates)
- .where(eq(tagsPlant.id, update.tagId));
- } catch (error) {
- errors.push(`Error updating tag record ${update.tagId}: ${error}`);
- }
- }
- }
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(100);
-
- // 최종 결과 반환
- return {
- processedCount,
- excludedCount,
- totalEntries: tagEntries.length,
- formCreated,
- errors: errors.length > 0 ? errors : undefined
- };
- });
-
- } catch (error: any) {
- console.error("Tag import error:", error);
- throw error;
- }
-}
-/**
- * SEDP API에서 태그 데이터 가져오기
- *
- * @param projectCode 프로젝트 코드
- * @param formCode 양식 코드
- * @returns API 응답 데이터
- */
-async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> {
- try {
- // Get the token
- const apiKey = await getSEDPToken();
-
- // Define the API base URL
- const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
-
- // Make the API call
- const response = await fetch(
- `${SEDP_API_BASE_URL}/Data/GetPubData`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- REG_TYPE_ID: formCode,
- // TODO: 이창국 프로 요청으로, ContainDeleted: true로 변경예정, EDP에서 삭제된 데이터도 가져올 수 있어야 한다고 함.
- // 삭제된 게 들어오면 eVCP내에서 지우거나, 비활성화 하는 등의 처리를 해야 할 걸로 보임
- ContainDeleted: false
- })
- }
- );
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`);
- }
-
- const data = await response.json();
- return data;
- } catch (error: any) {
- console.error('Error calling SEDP API:', error);
- throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`);
- }
-} \ No newline at end of file
diff --git a/lib/sedp/get-tags-plant.ts b/lib/sedp/get-tags-plant.ts
deleted file mode 100644
index d1957db4..00000000
--- a/lib/sedp/get-tags-plant.ts
+++ /dev/null
@@ -1,639 +0,0 @@
-import db from "@/db/db";
-import {
- tagsPlant,
- formsPlant,
- formEntriesPlant,
- items,
- tagTypeClassFormMappings,
- projects,
- tagTypes,
- tagClasses,
-} from "@/db/schema";
-import { eq, and, like, inArray } from "drizzle-orm";
-import { revalidateTag } from "next/cache"; // 추가
-import { getSEDPToken } from "./sedp-token";
-
-/**
- * 태그 가져오기 서비스 함수
- * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장
- * TAG_IDX를 기준으로 태그를 식별합니다.
- *
- * @param projectCode 계약 아이템 ID (contractItemId)
- * @param packageCode 계약 아이템 ID (contractItemId)
- * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수
- * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등)
- */
-export async function importTagsFromSEDP(
- projectCode: string,
- packageCode: string,
- progressCallback?: (progress: number) => void,
- mode?: string
-): Promise<{
- processedCount: number;
- excludedCount: number;
- totalEntries: number;
- errors?: string[];
-}> {
- try {
- // 진행 상황 보고
- if (progressCallback) progressCallback(5);
-
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
-
-
- // 프로젝트 ID 획득
- const projectId = project?.id;
-
- // Step 1-2: Get the item using itemId from contractItem
- const item = await db.query.items.findFirst({
- where: and(eq(items.ProjectNo, projectCode), eq(items.packageCode, packageCode))
- });
-
- if (!item) {
- throw new Error(`Item with ID ${item?.id} not found`);
- }
-
- const itemCode = item.itemCode;
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(10);
-
- // 기본 매핑 검색 - 모든 모드에서 사용
- const baseMappings = await db.query.tagTypeClassFormMappings.findMany({
- where: and(
- like(tagTypeClassFormMappings.remark, `%${itemCode}%`),
- eq(tagTypeClassFormMappings.projectId, projectId)
- )
- });
-
- if (baseMappings.length === 0) {
- throw new Error(`No mapping found for item code ${itemCode}`);
- }
-
- // Step 2: Find the mapping entries - 모드에 따라 다른 조건 적용
- let mappings = [];
-
- if (mode === 'IM') {
- // IM 모드일 때는 먼저 SEDP에서 태그 데이터를 가져와 TAG_TYPE_ID 리스트 확보
-
- // 프로젝트 코드 가져오기
- const project = await db.query.projects.findFirst({
- where: eq(projects.id, projectId)
- });
-
- if (!project) {
- throw new Error(`Project with ID ${projectId} not found`);
- }
-
- // 각 매핑의 formCode에 대해 태그 데이터 조회
- const tagTypeIds = new Set<string>();
-
- for (const mapping of baseMappings) {
- try {
- // SEDP에서 태그 데이터 가져오기
- const tagData = await fetchTagDataFromSEDP(project.code, mapping.formCode);
-
- // 첫 번째 키를 테이블 이름으로 사용
- const tableName = Object.keys(tagData)[0];
- const tagEntries = tagData[tableName];
-
- if (Array.isArray(tagEntries)) {
- // 모든 태그에서 TAG_TYPE_ID 수집
- for (const entry of tagEntries) {
- if (entry.TAG_TYPE_ID && entry.TAG_TYPE_ID !== "") {
- tagTypeIds.add(entry.TAG_TYPE_ID);
- }
- }
- }
- } catch (error) {
- console.error(`Error fetching tag data for formCode ${mapping.formCode}:`, error);
- }
- }
-
- if (tagTypeIds.size === 0) {
- throw new Error('No valid TAG_TYPE_ID found in SEDP tag data');
- }
-
- // 수집된 TAG_TYPE_ID로 tagTypes에서 정보 조회
- const tagTypeInfo = await db.query.tagTypes.findMany({
- where: and(
- inArray(tagTypes.code, Array.from(tagTypeIds)),
- eq(tagTypes.projectId, projectId)
- )
- });
-
- if (tagTypeInfo.length === 0) {
- throw new Error('No matching tag types found for the collected TAG_TYPE_IDs');
- }
-
- // 태그 타입 설명 수집
- const tagLabels = tagTypeInfo.map(tt => tt.description);
-
- // IM 모드에 맞는 매핑 조회 - ep가 "IMEP"인 항목만
- mappings = await db.query.tagTypeClassFormMappings.findMany({
- where: and(
- inArray(tagTypeClassFormMappings.tagTypeLabel, tagLabels),
- eq(tagTypeClassFormMappings.projectId, projectId),
- eq(tagTypeClassFormMappings.ep, "IMEP")
- )
- });
-
- } else {
- // ENG 모드 또는 기본 모드일 때 - 기본 매핑 사용
- mappings = [...baseMappings];
-
- // ENG 모드에서는 ep 필드가 "IMEP"가 아닌 매핑만 필터링
- if (mode === 'ENG') {
- mappings = mappings.filter(mapping => mapping.ep !== "IMEP");
- }
- }
-
- // 매핑이 없는 경우 모드에 따라 다른 오류 메시지 사용
- if (mappings.length === 0) {
- if (mode === 'IM') {
- throw new Error('No suitable mappings found for IM mode');
- } else {
- throw new Error(`No mapping found for item code ${itemCode}`);
- }
- }
-
- // 진행 상황 보고
- if (progressCallback) progressCallback(15);
-
- // 결과 누적을 위한 변수들 초기화
- let totalProcessedCount = 0;
- let totalExcludedCount = 0;
- let totalEntriesCount = 0;
- const allErrors: string[] = [];
-
- // 각 매핑에 대해 처리
- for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) {
- const mapping = mappings[mappingIndex];
-
- // Step 3: Get the project code
- const project = await db.query.projects.findFirst({
- where: eq(projects.id, mapping.projectId)
- });
-
- if (!project) {
- allErrors.push(`Project with ID ${mapping.projectId} not found`);
- continue; // 다음 매핑으로 진행
- }
-
- // IM 모드에서는 baseMappings에서 같은 formCode를 가진 매핑을 찾음
- let formCode = mapping.formCode;
- if (mode === 'IM') {
- // baseMapping에서 동일한 formCode를 가진 매핑 찾기
- const originalMapping = baseMappings.find(
- baseMapping => baseMapping.formCode === mapping.formCode
- );
-
- // 찾았으면 해당 formCode 사용, 못 찾았으면 현재 매핑의 formCode 유지
- if (originalMapping) {
- formCode = originalMapping.formCode;
- }
- }
-
- // 진행 상황 보고 - 매핑별 진행률 조정
- if (progressCallback) {
- const baseProgress = 15;
- const mappingProgress = Math.floor(15 * (mappingIndex + 1) / mappings.length);
- progressCallback(baseProgress + mappingProgress);
- }
-
- // Step 4: Find the form ID
- const form = await db.query.formsPlant.findFirst({
- where: and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formCode)
- )
- });
-
- let formId;
-
- // If form doesn't exist, create it
- if (!form) {
- // 폼이 없는 경우 새로 생성 - 모드에 따른 필드 설정
- const insertValues: any = {
- projectCode,
- packageCode,
- formCode: formCode,
- formName: mapping.formName
- };
-
- // 모드 정보가 있으면 해당 필드 설정
- if (mode) {
- if (mode === "ENG") {
- insertValues.eng = true;
- } else if (mode === "IM") {
- insertValues.im = true;
- if (mapping.remark && mapping.remark.includes("VD_")) {
- insertValues.eng = true;
- }
- }
- }
-
- const insertResult = await db.insert(formsPlant).values(insertValues).returning({ id: formsPlant.id });
-
- if (insertResult.length === 0) {
- allErrors.push(`Failed to create form record for formCode ${formCode}`);
- continue; // 다음 매핑으로 진행
- }
-
- formId = insertResult[0].id;
- } else {
- // 폼이 이미 존재하는 경우 - 필요시 모드 필드 업데이트
- formId = form.id;
-
- if (mode) {
- let shouldUpdate = false;
- const updateValues: any = {};
-
- if (mode === "ENG" && form.eng !== true) {
- updateValues.eng = true;
- shouldUpdate = true;
- } else if (mode === "IM" && form.im !== true) {
- updateValues.im = true;
- shouldUpdate = true;
- }
-
- if (shouldUpdate) {
- await db.update(formsPlant)
- .set({
- ...updateValues,
- updatedAt: new Date()
- })
- .where(eq(formsPlant.id, formId));
-
- console.log(`Updated form ${formId} with ${mode} mode enabled`);
- }
- }
- }
-
- // 진행 상황 보고 - 매핑별 진행률 조정
- if (progressCallback) {
- const baseProgress = 30;
- const mappingProgress = Math.floor(20 * (mappingIndex + 1) / mappings.length);
- progressCallback(baseProgress + mappingProgress);
- }
-
- try {
- // Step 5: Call the external API to get tag data
- const tagData = await fetchTagDataFromSEDP(projectCode, baseMappings[0].formCode);
-
- // 진행 상황 보고
- if (progressCallback) {
- const baseProgress = 50;
- const mappingProgress = Math.floor(10 * (mappingIndex + 1) / mappings.length);
- progressCallback(baseProgress + mappingProgress);
- }
-
- // Step 6: Process the data and insert into the tags table
- let processedCount = 0;
- let excludedCount = 0;
-
- // Get the first key from the response as the table name
- const tableName = Object.keys(tagData)[0];
- const tagEntries = tagData[tableName];
-
- if (!Array.isArray(tagEntries) || tagEntries.length === 0) {
- allErrors.push(`No tag data found in the API response for formCode ${baseMappings[0].formCode}`);
- continue; // 다음 매핑으로 진행
- }
-
- const entriesCount = tagEntries.length;
- totalEntriesCount += entriesCount;
-
- // formEntries를 위한 데이터 수집
- const newTagsForFormEntry: Array<{
- TAG_IDX: string; // 변경: TAG_NO → TAG_IDX
- TAG_NO?: string; // TAG_NO도 함께 저장 (편집 가능한 필드)
- TAG_DESC: string | null;
- status: string;
- [key: string]: any;
- }> = [];
-
- const registerResponse = await fetch(
- `${SEDP_API_BASE_URL}/Register/GetByID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- TYPE_ID: baseMappings[0].formCode, // 또는 mapping.formCode
- ContainDeleted: false
- })
- }
- )
-
- if (!registerResponse.ok) {
- allErrors.push(`Failed to fetch register details for ${baseMappings[0].formCode}`)
- continue
- }
-
- const registerDetail: Register = await registerResponse.json()
-
- // ✅ MAP_ATT에서 허용된 ATT_ID 목록 추출
- const allowedAttIds = new Set<string>()
- if (Array.isArray(registerDetail.MAP_ATT)) {
- for (const mapAttr of registerDetail.MAP_ATT) {
- if (mapAttr.ATT_ID) {
- allowedAttIds.add(mapAttr.ATT_ID)
- }
- }
- }
-
-
- // Process each tag entry
- for (let i = 0; i < tagEntries.length; i++) {
- try {
- const entry = tagEntries[i];
-
- // TAG_IDX가 없는 경우 제외 (변경: TAG_NO → TAG_IDX 체크)
- if (!entry.TAG_IDX) {
- excludedCount++;
- totalExcludedCount++;
-
- // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트)
- if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
- const baseProgress = 60;
- const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
- progressCallback(baseProgress + entryProgress);
- }
-
- continue; // 이 항목은 건너뜀
- }
-
- const attributes: Record<string, string> = {}
- if (Array.isArray(entry.ATTRIBUTES)) {
- for (const attr of entry.ATTRIBUTES) {
- // MAP_ATT에 정의된 ATT_ID만 포함
- if (attr.ATT_ID && allowedAttIds.has(attr.ATT_ID)) {
- if (attr.VALUE !== null && attr.VALUE !== undefined) {
- attributes[attr.ATT_ID] = String(attr.VALUE)
- }
- }
- }
- }
-
-
- // TAG_TYPE_ID가 null이거나 빈 문자열인 경우 제외
- if (entry.TAG_TYPE_ID === null || entry.TAG_TYPE_ID === "") {
- excludedCount++;
- totalExcludedCount++;
-
- // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트)
- if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
- const baseProgress = 60;
- const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
- progressCallback(baseProgress + entryProgress);
- }
-
- continue; // 이 항목은 건너뜀
- }
-
- // Get tag type description
- const tagType = await db.query.tagTypes.findFirst({
- where: and(
- eq(tagTypes.code, entry.TAG_TYPE_ID),
- eq(tagTypes.projectId, mapping.projectId)
- )
- });
-
- // Get tag class label
- const tagClass = await db.query.tagClasses.findFirst({
- where: and(
- eq(tagClasses.code, entry.CLS_ID),
- eq(tagClasses.projectId, mapping.projectId)
- )
- });
-
- // Insert or update the tag - tagIdx 필드 추가
- await db.insert(tagsPlant).values({
- projectCode,
- packageCode,
- formId: formId,
- tagIdx: entry.TAG_IDX,
- tagNo: entry.TAG_NO || entry.TAG_IDX,
- tagType: tagType?.description || entry.TAG_TYPE_ID,
- tagClassId: tagClass?.id,
- class: tagClass?.label || entry.CLS_ID,
- description: entry.TAG_DESC,
- attributes: attributes, // JSONB로 저장
- }).onConflictDoUpdate({
- target: [tagsPlant.projectCode, tagsPlant.packageCode, tagsPlant.tagIdx],
- set: {
- formId: formId,
- tagNo: entry.TAG_NO || entry.TAG_IDX,
- tagType: tagType?.description || entry.TAG_TYPE_ID,
- class: tagClass?.label || entry.CLS_ID,
- description: entry.TAG_DESC,
- attributes: attributes, // JSONB 업데이트
- updatedAt: new Date()
- }
- })
- // formEntries용 데이터 수집
- const tagDataForFormEntry = {
- TAG_IDX: entry.TAG_IDX, // 변경: TAG_NO → TAG_IDX
- TAG_NO: entry.TAG_NO || entry.TAG_IDX, // TAG_NO도 함께 저장
- TAG_DESC: entry.TAG_DESC || null,
- status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시
- source: "S-EDP" // 태그 출처 (불변) - S-EDP에서 가져옴
- };
-
- // ATTRIBUTES가 있으면 추가 (SHI 필드들)
- if (Array.isArray(entry.ATTRIBUTES)) {
- for (const attr of entry.ATTRIBUTES) {
- if (attr.ATT_ID && attr.VALUE !== null && attr.VALUE !== undefined) {
- tagDataForFormEntry[attr.ATT_ID] = attr.VALUE;
- }
- }
- }
-
- newTagsForFormEntry.push(tagDataForFormEntry);
-
- processedCount++;
- totalProcessedCount++;
-
- // 주기적으로 진행 상황 보고
- if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
- const baseProgress = 60;
- const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
- progressCallback(baseProgress + entryProgress);
- }
- } catch (error: any) {
- console.error(`Error processing tag entry:`, error);
- allErrors.push(error.message || 'Unknown error');
- }
- }
-
- // Step 7: formEntries 업데이트 - TAG_IDX 기준으로 변경
- if (newTagsForFormEntry.length > 0) {
- try {
- // 기존 formEntry 가져오기
- const existingEntry = await db.query.formEntriesPlant.findFirst({
- where: and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
- )
- });
-
- if (existingEntry && existingEntry.id) {
- // 기존 formEntry가 있는 경우
- let existingData: Array<{
- TAG_IDX?: string; // 추가: TAG_IDX 필드
- TAG_NO?: string;
- TAG_DESC?: string;
- status?: string;
- [key: string]: any;
- }> = [];
-
- if (Array.isArray(existingEntry.data)) {
- existingData = existingEntry.data;
- }
-
- // 기존 TAG_IDX들 추출 (변경: TAG_NO → TAG_IDX)
- const existingTagIdxs = new Set(
- existingData
- .map(item => item.TAG_IDX)
- .filter(tagIdx => tagIdx !== undefined && tagIdx !== null)
- );
-
- // 중복되지 않은 새 태그들만 필터링 (변경: TAG_NO → TAG_IDX)
- const newUniqueTagsData = newTagsForFormEntry.filter(
- tagData => !existingTagIdxs.has(tagData.TAG_IDX)
- );
-
- // 기존 태그들의 status와 ATTRIBUTES 업데이트 (변경: TAG_NO → TAG_IDX)
- const updatedExistingData = existingData.map(existingItem => {
- const newTagData = newTagsForFormEntry.find(
- newItem => newItem.TAG_IDX === existingItem.TAG_IDX
- );
-
- if (newTagData) {
- // 기존 태그가 있으면 SEDP 데이터로 업데이트
- return {
- ...existingItem,
- ...newTagData,
- TAG_IDX: existingItem.TAG_IDX // TAG_IDX는 유지
- };
- }
-
- return existingItem;
- });
-
- const finalData = [...updatedExistingData, ...newUniqueTagsData];
-
- await db
- .update(formEntriesPlant)
- .set({
- data: finalData,
- updatedAt: new Date()
- })
- .where(eq(formEntriesPlant.id, existingEntry.id));
-
- console.log(`[IMPORT SEDP] Updated formEntry with ${newUniqueTagsData.length} new tags, updated ${updatedExistingData.length - newUniqueTagsData.length} existing tags for form ${formCode}`);
- } else {
- // formEntry가 없는 경우 새로 생성
- await db.insert(formEntriesPlant).values({
- formCode: formCode,
- projectCode,
- packageCode,
- data: newTagsForFormEntry,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
-
- console.log(`[IMPORT SEDP] Created new formEntry with ${newTagsForFormEntry.length} tags for form ${formCode}`);
- }
-
- // 캐시 무효화
- revalidateTag(`form-data-${formCode}-${packageId}`);
- } catch (formEntryError) {
- console.error(`[IMPORT SEDP] Error updating formEntry for form ${formCode}:`, formEntryError);
- allErrors.push(`Error updating formEntry for form ${formCode}: ${formEntryError}`);
- }
- }
-
- } catch (error: any) {
- console.error(`Error processing mapping for formCode ${formCode}:`, error);
- allErrors.push(`Error with formCode ${formCode}: ${error.message || 'Unknown error'}`);
- }
- }
-
- // 모든 매핑 처리 완료 - 진행률 100%
- if (progressCallback) {
- progressCallback(100);
- }
-
- // 최종 결과 반환
- return {
- processedCount: totalProcessedCount,
- excludedCount: totalExcludedCount,
- totalEntries: totalEntriesCount,
- errors: allErrors.length > 0 ? allErrors : undefined
- };
- } catch (error: any) {
- console.error("Tag import error:", error);
- throw error;
- }
-}
-
-/**
- * SEDP API에서 태그 데이터 가져오기
- *
- * @param projectCode 프로젝트 코드
- * @param formCode 양식 코드
- * @returns API 응답 데이터
- */
-async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> {
- try {
- // Get the token
- const apiKey = await getSEDPToken();
-
- // Define the API base URL
- const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
-
- // Make the API call
- const response = await fetch(
- `${SEDP_API_BASE_URL}/Data/GetPubData`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- REG_TYPE_ID: formCode,
- ContainDeleted: false
- })
- }
- );
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`);
- }
-
- const data = await response.json();
- return data;
- } catch (error: any) {
- console.error('Error calling SEDP API:', error);
- throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`);
- }
-} \ No newline at end of file
diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts
index a6d473ad..904d27ba 100644
--- a/lib/sedp/sync-form.ts
+++ b/lib/sedp/sync-form.ts
@@ -94,7 +94,7 @@ interface Register {
SEQ: number;
CMPLX_YN: boolean;
CMPL_SETT: any | null;
- MAP_ATT: MapAttribute2[];
+ MAP_ATT: any[];
MAP_CLS_ID: string[];
MAP_OPER: any | null;
LNK_ATT: LinkAttribute[];
@@ -157,13 +157,6 @@ interface MapAttribute {
INOUT: string | null;
}
-interface MapAttribute2 {
- ATT_ID: string;
- VALUE: string;
- IS_PARA: boolean;
- OPER: string | null;
-}
-
interface Attribute {
PROJ_NO: string;
ATT_ID: string;
diff --git a/lib/tags-plant/column-builder.service.ts b/lib/tags-plant/column-builder.service.ts
deleted file mode 100644
index 9a552d6e..00000000
--- a/lib/tags-plant/column-builder.service.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// lib/vendor-data-plant/column-builder.service.ts
-import { ColumnDef } from "@tanstack/react-table"
-import { Tag } from "@/db/schema/vendorData"
-
-/**
- * 동적 속성 컬럼 생성 (ATT_ID만 사용, 라벨 없음)
- */
-export function createDynamicAttributeColumns(
- attributeKeys: string[]
-): ColumnDef<Tag>[] {
- return attributeKeys.map(key => ({
- id: `attr_${key}`,
- accessorFn: (row: Tag) => {
- if (row.attributes && typeof row.attributes === 'object') {
- return (row.attributes as Record<string, string>)[key] || ''
- }
- return ''
- },
- header: key, // 단순 문자열로 반환
- cell: ({ getValue }) => {
- const value = getValue()
- return value as string || "-"
- },
- meta: {
- excelHeader: key
- },
- enableSorting: true,
- enableColumnFilter: true,
- filterFn: "includesString",
- enableResizing: true,
- minSize: 100,
- size: 150,
- }))
-} \ No newline at end of file
diff --git a/lib/tags-plant/queries.ts b/lib/tags-plant/queries.ts
deleted file mode 100644
index a0d28b1e..00000000
--- a/lib/tags-plant/queries.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-// lib/vendor-data-plant/queries.ts
-"use server"
-
-import db from "@/db/db"
-
-import { tagsPlant } from "@/db/schema/vendorData"
-import { eq, and } from "drizzle-orm"
-
-/**
- * 모든 태그 가져오기 (클라이언트 렌더링용)
- */
-export async function getAllTagsPlant(
- projectCode: string,
- packageCode: string
-) {
- try {
- const tags = await db
- .select()
- .from(tagsPlant)
- .where(
- and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.packageCode, packageCode)
- )
- )
- .orderBy(tagsPlant.createdAt)
-
- return tags
- } catch (error) {
- console.error("Error fetching all tags:", error)
- return []
- }
-}
-
-/**
- * 고유 속성 키 추출
- */
-export async function getUniqueAttributeKeys(
- projectCode: string,
- packageCode: string
-): Promise<string[]> {
- try {
- const result = await db
- .select({
- attributes: tagsPlant.attributes
- })
- .from(tagsPlant)
- .where(
- and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.packageCode, packageCode)
- )
- )
-
- const allKeys = new Set<string>()
-
- for (const row of result) {
- if (row.attributes && typeof row.attributes === 'object') {
- Object.keys(row.attributes).forEach(key => allKeys.add(key))
- }
- }
-
- return Array.from(allKeys).sort()
- } catch (error) {
- console.error("Error getting unique attribute keys:", error)
- return []
- }
-} \ No newline at end of file
diff --git a/lib/tags-plant/repository.ts b/lib/tags-plant/repository.ts
index bbe36f66..b5d48335 100644
--- a/lib/tags-plant/repository.ts
+++ b/lib/tags-plant/repository.ts
@@ -1,5 +1,5 @@
import db from "@/db/db";
-import { NewTag, tags, tagsPlant } from "@/db/schema/vendorData";
+import { NewTag, tags } from "@/db/schema/vendorData";
import {
eq,
inArray,
@@ -69,43 +69,3 @@ export async function deleteTagsByIds(
) {
return tx.delete(tags).where(inArray(tags.id, ids));
}
-
-
-export async function selectTagsPlant(
- tx: PgTransaction<any, any, any>,
- params: {
- where?: any; // drizzle-orm의 조건식 (and, eq...) 등
- orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[];
- offset?: number;
- limit?: number;
- }
-) {
- const { where, orderBy, offset = 0, limit = 10 } = params;
-
- return tx
- .select()
- .from(tagsPlant)
- .where(where)
- .orderBy(...(orderBy ?? []))
- .offset(offset)
- .limit(limit);
-}
-/** 총 개수 count */
-export async function countTagsPlant(
- tx: PgTransaction<any, any, any>,
- where?: any
-) {
- const res = await tx.select({ count: count() }).from(tagsPlant).where(where);
- return res[0]?.count ?? 0;
-}
-
-export async function insertTagPlant(
- tx: PgTransaction<any, any, any>,
- data: NewTag // DB와 동일한 insert 가능한 타입
- ) {
- // returning() 사용 시 배열로 돌아오므로 [0]만 리턴
- return tx
- .insert(tagsPlant)
- .values(data)
- .returning({ id: tagsPlant.id, createdAt: tagsPlant.createdAt });
- } \ No newline at end of file
diff --git a/lib/tags-plant/service.ts b/lib/tags-plant/service.ts
index 02bd33be..778ab89d 100644
--- a/lib/tags-plant/service.ts
+++ b/lib/tags-plant/service.ts
@@ -1,14 +1,14 @@
"use server"
import db from "@/db/db"
-import { formEntries, forms,items,formsPlant, tagClasses, tags, tagsPlant, tagSubfieldOptions, tagSubfields, tagTypes,formEntriesPlant } from "@/db/schema"
+import { formEntries, forms, tagClasses, tags, tagSubfieldOptions, tagSubfields, tagTypes } from "@/db/schema/vendorData"
// import { eq } from "drizzle-orm"
import { createTagSchema, GetTagsSchema, updateTagSchema, UpdateTagSchema, type CreateTagSchema } from "./validations"
import { revalidateTag, unstable_noStore } from "next/cache";
import { filterColumns } from "@/lib/filter-columns";
import { unstable_cache } from "@/lib/unstable-cache";
import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, sql, ne, count, isNull } from "drizzle-orm";
-import { countTags, insertTag, selectTags, selectTagsPlant, countTagsPlant,insertTagPlant } from "./repository";
+import { countTags, insertTag, selectTags } from "./repository";
import { getErrorMessage } from "../handle-error";
import { getFormMappingsByTagType } from './form-mapping-service';
import { contractItems, contracts } from "@/db/schema/contract";
@@ -32,8 +32,7 @@ function generateTagIdx(): string {
return randomBytes(12).toString('hex'); // 12바이트 = 24자리 16진수
}
-
-export async function getTagsPlant(input: GetTagsSchema, projectCode: string,packageCode: string ) {
+export async function getTags(input: GetTagsSchema, packagesId: number) {
// return unstable_cache(
// async () => {
@@ -42,7 +41,7 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac
// (1) advancedWhere
const advancedWhere = filterColumns({
- table: tagsPlant,
+ table: tags,
filters: input.filters,
joinOperator: input.joinOperator,
});
@@ -52,31 +51,31 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac
if (input.search) {
const s = `%${input.search}%`;
globalWhere = or(
- ilike(tagsPlant.tagNo, s),
- ilike(tagsPlant.tagType, s),
- ilike(tagsPlant.description, s)
+ ilike(tags.tagNo, s),
+ ilike(tags.tagType, s),
+ ilike(tags.description, s)
);
}
- // (4) 최종 projectCode
- const finalWhere = and(advancedWhere, globalWhere, eq(tagsPlant.projectCode, projectCode), eq(tagsPlant.packageCode, packageCode));
+ // (4) 최종 where
+ const finalWhere = and(advancedWhere, globalWhere, eq(tags.contractItemId, packagesId));
// (5) 정렬
const orderBy =
input.sort.length > 0
? input.sort.map((item) =>
- item.desc ? desc(tagsPlant[item.id]) : asc(tagsPlant[item.id])
+ item.desc ? desc(tags[item.id]) : asc(tags[item.id])
)
- : [asc(tagsPlant.createdAt)];
+ : [asc(tags.createdAt)];
// 트랜잭션 내부에서 Repository 호출
const { data, total } = await db.transaction(async (tx) => {
- const data = await selectTagsPlant(tx, {
+ const data = await selectTags(tx, {
where: finalWhere,
orderBy,
offset,
limit: input.perPage,
});
- const total = await countTagsPlant(tx, finalWhere);
+ const total = await countTags(tx, finalWhere);
return { data, total };
@@ -102,10 +101,9 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac
export async function createTag(
formData: CreateTagSchema,
- projectCode: string,
- packageCode: string,
+ selectedPackageId: number | null
) {
- if (!projectCode) {
+ if (!selectedPackageId) {
return { error: "No selectedPackageId provided" }
}
@@ -121,23 +119,33 @@ export async function createTag(
try {
// 하나의 트랜잭션에서 모든 작업 수행
return await db.transaction(async (tx) => {
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ // 1) 선택된 contractItem의 contractId 가져오기
+ const contractItemResult = await tx
+ .select({
+ contractId: contractItems.contractId,
+ projectId: contracts.projectId // projectId 추가
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1)
- const projectId = project.id
+ if (contractItemResult.length === 0) {
+ return { error: "Contract item not found" }
+ }
+
+ const contractId = contractItemResult[0].contractId
+ const projectId = contractItemResult[0].projectId
// 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인
const duplicateCheck = await tx
.select({ count: sql<number>`count(*)` })
- .from(tagsPlant)
+ .from(tags)
+ .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
.where(
and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.tagNo, validated.data.tagNo)
+ eq(contractItems.contractId, contractId),
+ eq(tags.tagNo, validated.data.tagNo)
)
)
@@ -174,16 +182,16 @@ export async function createTag(
const createdOrExistingForms: CreatedOrExistingForm[] = []
if (formMappings && formMappings.length > 0) {
+ console.log(selectedPackageId, formMappings)
for (const formMapping of formMappings) {
// 4-1) 이미 존재하는 폼인지 확인
const existingForm = await tx
- .select({ id: formsPlant.id, im: formsPlant.im, eng: formsPlant.eng }) // eng 필드도 추가로 조회
- .from(formsPlant)
+ .select({ id: forms.id, im: forms.im, eng: forms.eng }) // eng 필드도 추가로 조회
+ .from(forms)
.where(
and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formMapping.formCode)
+ eq(forms.contractItemId, selectedPackageId),
+ eq(forms.formCode, formMapping.formCode)
)
)
.limit(1)
@@ -211,9 +219,9 @@ export async function createTag(
if (shouldUpdate) {
await tx
- .update(formsPlant)
+ .update(forms)
.set(updateValues)
- .where(eq(formsPlant.id, formId))
+ .where(eq(forms.id, formId))
console.log(`Form ${formId} updated with:`, updateValues)
}
@@ -227,8 +235,7 @@ export async function createTag(
} else {
// 존재하지 않으면 새로 생성
const insertValues: any = {
- projectCode: projectCode,
- packageCode: packageCode,
+ contractItemId: selectedPackageId,
formCode: formMapping.formCode,
formName: formMapping.formName,
im: true,
@@ -240,9 +247,9 @@ export async function createTag(
}
const insertResult = await tx
- .insert(formsPlant)
+ .insert(forms)
.values(insertValues)
- .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName })
+ .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName })
console.log("insertResult:", insertResult)
formId = insertResult[0].id
@@ -266,9 +273,8 @@ export async function createTag(
console.log(`[CREATE TAG] Generated tagIdx: ${generatedTagIdx}`);
// 5) 새 Tag 생성 (tagIdx 추가)
- const [newTag] = await insertTagPlant(tx, {
- packageCode:packageCode,
- projectCode:projectCode,
+ const [newTag] = await insertTag(tx, {
+ contractItemId: selectedPackageId,
formId: primaryFormId,
tagIdx: generatedTagIdx, // 🆕 생성된 16진수 24자리 추가
tagNo: validated.data.tagNo,
@@ -277,6 +283,7 @@ export async function createTag(
description: validated.data.description ?? null,
})
+ console.log(`tags-${selectedPackageId}`, "create", newTag)
// 6) 생성된 각 form에 대해 formEntries에 데이터 추가 (TAG_IDX 포함)
for (const form of createdOrExistingForms) {
@@ -284,9 +291,8 @@ export async function createTag(
// 기존 formEntry 가져오기
const existingEntry = await tx.query.formEntries.findFirst({
where: and(
- eq(formEntriesPlant.formCode, form.formCode),
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode)
+ eq(formEntries.formCode, form.formCode),
+ eq(formEntries.contractItemId, selectedPackageId)
)
});
@@ -323,12 +329,12 @@ export async function createTag(
const updatedData = [...existingData, newTagData];
await tx
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedData,
updatedAt: new Date()
})
- .where(eq(formEntriesPlant.id, existingEntry.id));
+ .where(eq(formEntries.id, existingEntry.id));
console.log(`[CREATE TAG] Added tag ${validated.data.tagNo} with tagIdx ${generatedTagIdx} to existing formEntry for form ${form.formCode}`);
} else {
@@ -336,10 +342,9 @@ export async function createTag(
}
} else {
// formEntry가 없는 경우 새로 생성 (TAG_IDX 포함)
- await tx.insert(formEntriesPlant).values({
+ await tx.insert(formEntries).values({
formCode: form.formCode,
- projectCode: projectCode,
- packageCode: packageCode,
+ contractItemId: selectedPackageId,
data: [newTagData],
createdAt: new Date(),
updatedAt: new Date(),
@@ -353,6 +358,16 @@ export async function createTag(
}
}
+ // 7) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시)
+ revalidateTag(`tags-${selectedPackageId}`)
+ revalidateTag(`forms-${selectedPackageId}-ENG`)
+ revalidateTag("tags")
+
+ // 생성된 각 form의 캐시도 무효화
+ createdOrExistingForms.forEach(form => {
+ revalidateTag(`form-data-${form.formCode}-${selectedPackageId}`)
+ })
+
// 8) 성공 시 반환 (tagIdx 추가)
return {
success: true,
@@ -651,11 +666,10 @@ export async function createTagInForm(
export async function updateTag(
formData: UpdateTagSchema & { id: number },
- projectCode: string,
- packageCode: string,
+ selectedPackageId: number | null
) {
- if (!projectCode) {
- return { error: "No projectCode provided" }
+ if (!selectedPackageId) {
+ return { error: "No selectedPackageId provided" }
}
if (!formData.id) {
@@ -687,25 +701,35 @@ export async function updateTag(
const originalTag = existingTag[0]
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ // 2) 선택된 contractItem의 contractId 가져오기
+ const contractItemResult = await tx
+ .select({
+ contractId: contractItems.contractId,
+ projectId: contracts.projectId // projectId 추가
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1)
+
+ if (contractItemResult.length === 0) {
+ return { error: "Contract item not found" }
+ }
- const projectId = project.id
+ const contractId = contractItemResult[0].contractId
+ const projectId = contractItemResult[0].projectId
// 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인
if (originalTag.tagNo !== validated.data.tagNo) {
const duplicateCheck = await tx
.select({ count: sql<number>`count(*)` })
- .from(tagsPlant)
+ .from(tags)
+ .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
.where(
and(
- eq(tagsPlant.projectCode, projectCode),
- eq(tagsPlant.tagNo, validated.data.tagNo),
- ne(tagsPlant.id, formData.id) // 자기 자신은 제외
+ eq(contractItems.contractId, contractId),
+ eq(tags.tagNo, validated.data.tagNo),
+ ne(tags.id, formData.id) // 자기 자신은 제외
)
)
@@ -750,12 +774,11 @@ export async function updateTag(
// 이미 존재하는 폼인지 확인
const existingForm = await tx
.select({ id: forms.id })
- .from(formsPlant)
+ .from(forms)
.where(
and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formMapping.formCode)
+ eq(forms.contractItemId, selectedPackageId),
+ eq(forms.formCode, formMapping.formCode)
)
)
.limit(1)
@@ -773,14 +796,13 @@ export async function updateTag(
} else {
// 존재하지 않으면 새로 생성
const insertResult = await tx
- .insert(formsPlant)
+ .insert(forms)
.values({
- projectCode,
- packageCode,
+ contractItemId: selectedPackageId,
formCode: formMapping.formCode,
formName: formMapping.formName,
})
- .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName })
+ .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName })
formId = insertResult[0].id
createdOrExistingForms.push({
@@ -801,10 +823,9 @@ export async function updateTag(
// 5) 태그 업데이트
const [updatedTag] = await tx
- .update(tagsPlant)
+ .update(tags)
.set({
- projectCode,
- packageCode,
+ contractItemId: selectedPackageId,
formId: primaryFormId,
tagNo: validated.data.tagNo,
class: validated.data.class,
@@ -812,9 +833,12 @@ export async function updateTag(
description: validated.data.description ?? null,
updatedAt: new Date(),
})
- .where(eq(tagsPlant.id, formData.id))
+ .where(eq(tags.id, formData.id))
.returning()
+ // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시)
+ revalidateTag(`tags-${selectedPackageId}`)
+ revalidateTag(`forms-${selectedPackageId}`)
revalidateTag("tags")
// 7) 성공 시 반환
@@ -843,8 +867,7 @@ export interface TagInputData {
// 새로운 서버 액션
export async function bulkCreateTags(
tagsfromExcel: TagInputData[],
- projectCode: string,
- packageCode: string
+ selectedPackageId: number
) {
unstable_noStore();
@@ -856,22 +879,31 @@ export async function bulkCreateTags(
// 단일 트랜잭션으로 모든 작업 처리
return await db.transaction(async (tx) => {
// 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만)
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ const contractItemResult = await tx
+ .select({
+ contractId: contractItems.contractId,
+ projectId: contracts.projectId // projectId 추가
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1);
+
+ if (contractItemResult.length === 0) {
+ return { error: "Contract item not found" };
+ }
- const projectId = project.id
+ const contractId = contractItemResult[0].contractId;
+ const projectId = contractItemResult[0].projectId; // projectId 추출
// 2. 모든 태그 번호 중복 검사 (한 번에)
const tagNos = tagsfromExcel.map(tag => tag.tagNo);
const duplicateCheck = await tx
.select({ tagNo: tags.tagNo })
.from(tags)
+ .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
.where(and(
- eq(tags.projectCode, projectCode),
+ eq(contractItems.contractId, contractId),
inArray(tags.tagNo, tagNos)
));
@@ -937,13 +969,12 @@ export async function bulkCreateTags(
for (const formMapping of formMappings) {
// 해당 폼이 이미 존재하는지 확인
const existingForm = await tx
- .select({ id: formsPlant.id, im: formsPlant.im })
- .from(formsPlant)
+ .select({ id: forms.id, im: forms.im })
+ .from(forms)
.where(
and(
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.formCode, formMapping.formCode)
+ eq(forms.contractItemId, selectedPackageId),
+ eq(forms.formCode, formMapping.formCode)
)
)
.limit(1);
@@ -956,9 +987,9 @@ export async function bulkCreateTags(
// im 필드 업데이트 (필요한 경우)
if (existingForm[0].im !== true) {
await tx
- .update(formsPlant)
+ .update(forms)
.set({ im: true })
- .where(eq(formsPlant.id, formId));
+ .where(eq(forms.id, formId));
}
createdOrExistingForms.push({
@@ -970,15 +1001,14 @@ export async function bulkCreateTags(
} else {
// 존재하지 않으면 새로 생성
const insertResult = await tx
- .insert(formsPlant)
+ .insert(forms)
.values({
- packageCode:packageCode,
- projectCode:projectCode,
+ contractItemId: selectedPackageId,
formCode: formMapping.formCode,
formName: formMapping.formName,
im: true
})
- .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName });
+ .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName });
formId = insertResult[0].id;
createdOrExistingForms.push({
@@ -1018,9 +1048,8 @@ export async function bulkCreateTags(
}
// 태그 생성
- const [newTag] = await insertTagPlant(tx, {
- packageCode:packageCode,
- projectCode:projectCode,
+ const [newTag] = await insertTag(tx, {
+ contractItemId: selectedPackageId,
formId: primaryFormId,
tagNo: tagData.tagNo,
class: tagData.class || "",
@@ -1038,15 +1067,14 @@ export async function bulkCreateTags(
});
}
- // 4. formEntriesPlant 업데이트 처리
+ // 4. formEntries 업데이트 처리
for (const [formCode, newTagsData] of tagsByFormCode.entries()) {
try {
// 기존 formEntry 가져오기
- const existingEntry = await tx.query.formEntriesPlant.findFirst({
+ const existingEntry = await tx.query.formEntries.findFirst({
where: and(
- eq(formEntriesPlant.formCode, formCode),
- eq(formEntriesPlant.packageCode, packageCode),
- eq(formEntriesPlant.projectCode, projectCode)
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, selectedPackageId)
)
});
@@ -1075,12 +1103,12 @@ export async function bulkCreateTags(
const updatedData = [...existingData, ...newUniqueTagsData];
await tx
- .update(formEntriesPlant)
+ .update(formEntries)
.set({
data: updatedData,
updatedAt: new Date()
})
- .where(eq(formEntriesPlant.id, existingEntry.id));
+ .where(eq(formEntries.id, existingEntry.id));
console.log(`[BULK CREATE] Added ${newUniqueTagsData.length} tags to existing formEntry for form ${formCode}`);
} else {
@@ -1088,10 +1116,9 @@ export async function bulkCreateTags(
}
} else {
// formEntry가 없는 경우 새로 생성
- await tx.insert(formEntriesPlant).values({
+ await tx.insert(formEntries).values({
formCode: formCode,
- projectCode:projectCode,
- packageCode:packageCode,
+ contractItemId: selectedPackageId,
data: newTagsData,
createdAt: new Date(),
updatedAt: new Date(),
@@ -1105,6 +1132,16 @@ export async function bulkCreateTags(
}
}
+ // 5. 캐시 무효화 (한 번만)
+ revalidateTag(`tags-${selectedPackageId}`);
+ revalidateTag(`forms-${selectedPackageId}`);
+ revalidateTag("tags");
+
+ // 업데이트된 모든 form의 캐시도 무효화
+ for (const formCode of tagsByFormCode.keys()) {
+ revalidateTag(`form-data-${formCode}-${selectedPackageId}`);
+ }
+
return {
success: true,
data: {
@@ -1123,8 +1160,7 @@ export async function bulkCreateTags(
/** 복수 삭제 */
interface RemoveTagsInput {
ids: number[];
- projectCode: string;
- packageCode: string;
+ selectedPackageId: number;
}
@@ -1142,29 +1178,36 @@ function removeTagFromDataJson(
export async function removeTags(input: RemoveTagsInput) {
unstable_noStore() // React 서버 액션 무상태 함수
- const { ids, projectCode, packageCode } = input
+ const { ids, selectedPackageId } = input
try {
await db.transaction(async (tx) => {
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
- const projectId = project.id;
+ const packageInfo = await tx
+ .select({
+ projectId: contracts.projectId
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1);
+
+ if (packageInfo.length === 0) {
+ throw new Error(`Contract item with ID ${selectedPackageId} not found`);
+ }
+
+ const projectId = packageInfo[0].projectId;
// 1) 삭제 대상 tag들을 미리 조회
const tagsToDelete = await tx
.select({
- id: tagsPlant.id,
- tagNo: tagsPlant.tagNo,
- tagType: tagsPlant.tagType,
- class: tagsPlant.class,
+ id: tags.id,
+ tagNo: tags.tagNo,
+ tagType: tags.tagType,
+ class: tags.class,
})
- .from(tagsPlant)
- .where(inArray(tagsPlant.id, ids))
+ .from(tags)
+ .where(inArray(tags.id, ids))
// 2) 태그 타입과 클래스의 고유 조합 추출
const uniqueTypeClassCombinations = [...new Set(
@@ -1179,14 +1222,13 @@ export async function removeTags(input: RemoveTagsInput) {
// 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인
const otherTagsWithSameTypeClass = await tx
.select({ count: count() })
- .from(tagsPlant)
+ .from(tags)
.where(
and(
- eq(tagsPlant.tagType, tagType),
- classValue ? eq(tagsPlant.class, classValue) : isNull(tagsPlant.class),
- not(inArray(tagsPlant.id, ids)),
- eq(tags.packageCode, packageCode),
- eq(tags.projectCode, projectCode) // 같은 contractItemId 내에서만 확인
+ eq(tags.tagType, tagType),
+ classValue ? eq(tags.class, classValue) : isNull(tags.class),
+ not(inArray(tags.id, ids)), // 현재 삭제 중인 태그들은 제외
+ eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인
)
)
@@ -1207,23 +1249,21 @@ export async function removeTags(input: RemoveTagsInput) {
if (otherTagsWithSameTypeClass[0].count === 0) {
// 폼 삭제
await tx
- .delete(formsPlant)
+ .delete(forms)
.where(
and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.formCode, formMapping.formCode)
+ eq(forms.contractItemId, selectedPackageId),
+ eq(forms.formCode, formMapping.formCode)
)
)
// formEntries 테이블에서도 해당 formCode 관련 데이터 삭제
await tx
- .delete(formEntriesPlant)
+ .delete(formEntries)
.where(
and(
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
- eq(formEntriesPlant.formCode, formMapping.formCode)
+ eq(formEntries.contractItemId, selectedPackageId),
+ eq(formEntries.formCode, formMapping.formCode)
)
)
}
@@ -1231,15 +1271,14 @@ export async function removeTags(input: RemoveTagsInput) {
else if (relevantTagNos.length > 0) {
const formEntryRecords = await tx
.select({
- id: formEntriesPlant.id,
- data: formEntriesPlant.data,
+ id: formEntries.id,
+ data: formEntries.data,
})
- .from(formEntriesPlant)
+ .from(formEntries)
.where(
and(
- eq(formEntriesPlant.projectCode, projectCode),
- eq(formEntriesPlant.packageCode, packageCode),
- eq(formEntriesPlant.formCode, formMapping.formCode)
+ eq(formEntries.contractItemId, selectedPackageId),
+ eq(formEntries.formCode, formMapping.formCode)
)
)
@@ -1266,6 +1305,9 @@ export async function removeTags(input: RemoveTagsInput) {
await tx.delete(tags).where(inArray(tags.id, ids))
})
+ // 5) 캐시 무효화
+ revalidateTag(`tags-${selectedPackageId}`)
+ revalidateTag(`forms-${selectedPackageId}`)
return { data: null, error: null }
} catch (err) {
@@ -1286,26 +1328,25 @@ export interface ClassOption {
* Class 옵션 목록을 가져오는 함수
* 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함
*/
-export async function getClassOptions(
- packageCode: string,
- projectCode: string
-): Promise<UpdatedClassOption[]> {
+export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> {
try {
- // 1. 프로젝트 정보 조회
- const projectInfo = await db
- .select()
- .from(projects)
- .where(eq(projects.code, projectCode))
+ // 1. 먼저 contractItems에서 projectId 조회
+ const packageInfo = await db
+ .select({
+ projectId: contracts.projectId
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .where(eq(contractItems.id, selectedPackageId))
.limit(1);
- if (projectInfo.length === 0) {
- throw new Error(`Project with code ${projectCode} not found`);
+ if (packageInfo.length === 0) {
+ throw new Error(`Contract item with ID ${selectedPackageId} not found`);
}
- const projectId = projectInfo[0].id;
-
+ const projectId = packageInfo[0].projectId;
- // 3. 태그 클래스들을 서브클래스 정보와 함께 조회
+ // 2. 태그 클래스들을 서브클래스 정보와 함께 조회
const tagClassesWithSubclasses = await db
.select({
id: tagClasses.id,
@@ -1319,8 +1360,8 @@ export async function getClassOptions(
.where(eq(tagClasses.projectId, projectId))
.orderBy(tagClasses.code);
- // 4. 태그 타입 정보도 함께 조회 (description을 위해)
- const tagTypesMap = new Map<string, string>();
+ // 3. 태그 타입 정보도 함께 조회 (description을 위해)
+ const tagTypesMap = new Map();
const tagTypesList = await db
.select({
code: tagTypes.code,
@@ -1329,24 +1370,21 @@ export async function getClassOptions(
.from(tagTypes)
.where(eq(tagTypes.projectId, projectId));
- tagTypesList.forEach((tagType) => {
+ tagTypesList.forEach(tagType => {
tagTypesMap.set(tagType.code, tagType.description);
});
- // 5. 클래스 옵션으로 변환
- const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(
- (cls) => ({
- value: cls.code,
- label: cls.label,
- code: cls.code,
- description: cls.label,
- tagTypeCode: cls.tagTypeCode,
- tagTypeDescription:
- tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode,
- subclasses: cls.subclasses || [],
- subclassRemark: cls.subclassRemark || {},
- })
- );
+ // 4. 클래스 옵션으로 변환
+ const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(cls => ({
+ value: cls.code,
+ label: cls.label,
+ code: cls.code,
+ description: cls.label,
+ tagTypeCode: cls.tagTypeCode,
+ tagTypeDescription: tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode,
+ subclasses: cls.subclasses || [],
+ subclassRemark: cls.subclassRemark || {},
+ }));
return classOptions;
} catch (error) {
@@ -1354,8 +1392,6 @@ export async function getClassOptions(
throw new Error("Failed to fetch class options");
}
}
-
-
interface SubFieldDef {
name: string
label: string
@@ -1367,20 +1403,26 @@ interface SubFieldDef {
export async function getSubfieldsByTagType(
tagTypeCode: string,
- projectCode: string,
+ selectedPackageId: number,
subclassRemark: string = "",
subclass: string = "",
) {
try {
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
- columns: {
- id: true
- }
- });
+ // 1. 먼저 contractItems에서 projectId 조회
+ const packageInfo = await db
+ .select({
+ projectId: contracts.projectId
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1);
+ if (packageInfo.length === 0) {
+ throw new Error(`Contract item with ID ${selectedPackageId} not found`);
+ }
- const projectId = project.id
+ const projectId = packageInfo[0].projectId;
// 2. 올바른 projectId를 사용하여 tagSubfields 조회
const rows = await db
@@ -1581,314 +1623,29 @@ export interface TagTypeOption {
label: string; // tagTypes.description 값
}
-export async function getProjectIdFromContractItemId(
- projectCode: string
-): Promise<number | null> {
+export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> {
try {
// First get the contractId from contractItems
- const project = await db.query.projects.findFirst({
- where: eq(projects.code, projectCode),
+ const contractItem = await db.query.contractItems.findFirst({
+ where: eq(contractItems.id, contractItemId),
columns: {
- id: true
+ contractId: true
}
});
- if (!project) return null;
-
- return project?.id || null;
- } catch (error) {
- console.error("Error fetching projectId:", error);
- return null;
- }
-}
-
-const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
-
+ if (!contractItem) return null;
-/**
- * Engineering 폼 목록 가져오기
- */
-export async function getEngineeringForms(
- projectCode: string,
- packageCode: string
-): Promise<FormInfo[]> {
- try {
- // 1. DB에서 eng=true인 폼 조회
- const existingForms = await db
- .select({
- formCode: formsPlant.formCode,
- formName: formsPlant.formName,
- })
- .from(formsPlant)
- .where(
- and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.eng, true)
- )
- )
-
- // DB에 데이터가 있으면 반환
- if (existingForms.length > 0) {
- return existingForms
- }
-
- // 2. DB에 없으면 SEDP API에서 가져오기
- const apiKey = await getSEDPToken()
-
- // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기
- const mappingResponse = await fetch(
- `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- TOOL_ID: "eVCP"
- })
- }
- )
-
- if (!mappingResponse.ok) {
- throw new Error(
- `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}`
- )
- }
-
- const mappingData = await mappingResponse.json()
- const registers: NewRegister[] = Array.isArray(mappingData)
- ? mappingData
- : [mappingData]
-
- // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링
- const matchingRegisters = registers.filter(register =>
- register.SCOPES.includes(packageCode)
- )
-
- if (matchingRegisters.length === 0) {
- console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`)
- return []
- }
-
- // 2-3. 각 레지스터의 상세 정보 가져오기
- const formInfos: FormInfo[] = []
- const formsToInsert: typeof formsPlant.$inferInsert[] = []
-
- for (const register of matchingRegisters) {
- try {
- const detailResponse = await fetch(
- `${SEDP_API_BASE_URL}/Register/GetByID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- TYPE_ID: register.REG_TYPE_ID,
- ContainDeleted: false
- })
- }
- )
-
- if (!detailResponse.ok) {
- console.error(
- `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}`
- )
- continue
- }
-
- const detail: RegisterDetail = await detailResponse.json()
-
- // DELETED가 true이거나 DESC가 없으면 스킵
- if (detail.DELETED || !detail.DESC) {
- continue
- }
-
- formInfos.push({
- formCode: detail.TYPE_ID,
- formName: detail.DESC
- })
-
- // DB 삽입용 데이터 준비
- formsToInsert.push({
- projectCode: projectCode,
- packageCode: packageCode,
- formCode: detail.TYPE_ID,
- formName: detail.DESC,
- eng: true,
- im: false
- })
- } catch (error) {
- console.error(
- `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`,
- error
- )
- continue
- }
- }
-
- // 2-4. DB에 저장
- if (formsToInsert.length > 0) {
- await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing()
- console.log(`${formsToInsert.length}개의 Engineering 폼을 DB에 저장했습니다.`)
- }
-
- return formInfos
- } catch (error) {
- console.error("Engineering 폼 가져오기 실패:", error)
- throw new Error("Failed to fetch engineering forms")
- }
-}
-
-/**
- * IM 폼 목록 가져오기
- */
-export async function getIMForms(
- projectCode: string,
- packageCode: string
-): Promise<FormInfo[]> {
- try {
- // 1. DB에서 im=true인 폼 조회
- const existingForms = await db
- .select({
- formCode: formsPlant.formCode,
- formName: formsPlant.formName,
- })
- .from(formsPlant)
- .where(
- and(
- eq(formsPlant.projectCode, projectCode),
- eq(formsPlant.packageCode, packageCode),
- eq(formsPlant.im, true)
- )
- )
-
- // DB에 데이터가 있으면 반환
- if (existingForms.length > 0) {
- return existingForms
- }
-
- // 2. DB에 없으면 SEDP API에서 가져오기
- const apiKey = await getSEDPToken()
-
- // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기
- const mappingResponse = await fetch(
- `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- TOOL_ID: "eVCP"
- })
- }
- )
-
- if (!mappingResponse.ok) {
- throw new Error(
- `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}`
- )
- }
-
- const mappingData = await mappingResponse.json()
- const registers: NewRegister[] = Array.isArray(mappingData)
- ? mappingData
- : [mappingData]
-
- // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링
- const matchingRegisters = registers.filter(register =>
- register.SCOPES.includes(packageCode)
- )
-
- if (matchingRegisters.length === 0) {
- console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`)
- return []
- }
-
- // 2-3. 각 레지스터의 상세 정보 가져오기
- const formInfos: FormInfo[] = []
- const formsToInsert: typeof formsPlant.$inferInsert[] = []
-
- for (const register of matchingRegisters) {
- try {
- const detailResponse = await fetch(
- `${SEDP_API_BASE_URL}/Register/GetByID`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'accept': '*/*',
- 'ApiKey': apiKey,
- 'ProjectNo': projectCode
- },
- body: JSON.stringify({
- ProjectNo: projectCode,
- TYPE_ID: register.REG_TYPE_ID,
- ContainDeleted: false
- })
- }
- )
-
- if (!detailResponse.ok) {
- console.error(
- `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}`
- )
- continue
- }
-
- const detail: RegisterDetail = await detailResponse.json()
-
- // DELETED가 true이거나 DESC가 없으면 스킵
- if (detail.DELETED || !detail.DESC) {
- continue
- }
-
- formInfos.push({
- formCode: detail.TYPE_ID,
- formName: detail.DESC
- })
-
- // DB 삽입용 데이터 준비
- formsToInsert.push({
- projectCode: projectCode,
- packageCode: packageCode,
- formCode: detail.TYPE_ID,
- formName: detail.DESC,
- eng: false,
- im: true
- })
- } catch (error) {
- console.error(
- `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`,
- error
- )
- continue
+ // Then get the projectId from contracts
+ const contract = await db.query.contracts.findFirst({
+ where: eq(contracts.id, contractItem.contractId),
+ columns: {
+ projectId: true
}
- }
-
- // 2-4. DB에 저장
- if (formsToInsert.length > 0) {
- await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing()
- console.log(`${formsToInsert.length}개의 IM 폼을 DB에 저장했습니다.`)
- }
+ });
- return formInfos
+ return contract?.projectId || null;
} catch (error) {
- console.error("IM 폼 가져오기 실패:", error)
- throw new Error("Failed to fetch IM forms")
+ console.error("Error fetching projectId:", error);
+ return null;
}
} \ No newline at end of file
diff --git a/lib/tags-plant/table/add-tag-dialog.tsx b/lib/tags-plant/table/add-tag-dialog.tsx
index 41731f63..9c82bf1a 100644
--- a/lib/tags-plant/table/add-tag-dialog.tsx
+++ b/lib/tags-plant/table/add-tag-dialog.tsx
@@ -61,7 +61,7 @@ import {
getClassOptions,
type ClassOption,
TagTypeOption,
-} from "@/lib/tags-plant/service"
+} from "@/lib/tags/service"
import { ScrollArea } from "@/components/ui/scroll-area"
// Updated to support multiple rows and subclass
@@ -98,11 +98,10 @@ interface UpdatedClassOption extends ClassOption {
}
interface AddTagDialogProps {
- projectCode: string
- packageCode: string
+ selectedPackageId: number
}
-export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
+export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
const router = useRouter()
const params = useParams()
const lng = (params?.lng as string) || "ko"
@@ -126,6 +125,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
const fieldIdsRef = React.useRef<Record<string, string>>({})
const classOptionIdsRef = React.useRef<Record<string, string>>({})
+ console.log(selectedPackageId, "tag")
// ---------------
// Load Class Options (서브클래스 정보 포함)
@@ -135,7 +135,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
setIsLoadingClasses(true)
try {
// getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정
- const result = await getClassOptions(packageCode, projectCode)
+ const result = await getClassOptions(selectedPackageId)
setClassOptions(result)
} catch (err) {
toast.error(t("toast.classOptionsLoadFailed"))
@@ -147,7 +147,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
if (open) {
loadClassOptions()
}
- }, [open, projectCode, packageCode])
+ }, [open, selectedPackageId])
// ---------------
// react-hook-form with fieldArray support for multiple rows
@@ -176,7 +176,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
setIsLoadingSubFields(true)
try {
// 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가)
- const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass)
+ const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark, subclass)
const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({
name: field.name,
label: field.label,
@@ -313,7 +313,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
// Submit handler for multiple tags (서브클래스 정보 포함)
// ---------------
async function onSubmit(data: MultiTagFormValues) {
- if (!projectCode) {
+ if (!selectedPackageId) {
toast.error(t("toast.noSelectedPackageId"));
return;
}
@@ -343,7 +343,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
};
try {
- const res = await createTag(tagData, projectCode, packageCode);
+ const res = await createTag(tagData, selectedPackageId);
if ("error" in res) {
console.log(res.error)
failedTags.push({ tag: row.tagNo, error: res.error });
diff --git a/lib/tags-plant/table/delete-tags-dialog.tsx b/lib/tags-plant/table/delete-tags-dialog.tsx
index 69a4f4a6..6a024cda 100644
--- a/lib/tags-plant/table/delete-tags-dialog.tsx
+++ b/lib/tags-plant/table/delete-tags-dialog.tsx
@@ -4,6 +4,7 @@ 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 {
@@ -26,15 +27,15 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
-import { removeTags } from "@/lib//tags-plant/service"
+
+import { removeTags } from "@/lib//tags/service"
import { Tag } from "@/db/schema/vendorData"
interface DeleteTasksDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
tags: Row<Tag>["original"][]
showTrigger?: boolean
- projectCode: string
- packageCode: string
+ selectedPackageId: number
onSuccess?: () => void
}
@@ -42,8 +43,7 @@ export function DeleteTagsDialog({
tags,
showTrigger = true,
onSuccess,
- projectCode,
- packageCode,
+ selectedPackageId,
...props
}: DeleteTasksDialogProps) {
const [isDeletePending, startDeleteTransition] = React.useTransition()
@@ -52,7 +52,7 @@ export function DeleteTagsDialog({
function onDelete() {
startDeleteTransition(async () => {
const { error } = await removeTags({
- ids: tags.map((tag) => tag.id),projectCode, packageCode
+ ids: tags.map((tag) => tag.id),selectedPackageId
})
if (error) {
diff --git a/lib/tags-plant/table/tag-table.tsx b/lib/tags-plant/table/tag-table.tsx
index 2fdcd5fc..1986d933 100644
--- a/lib/tags-plant/table/tag-table.tsx
+++ b/lib/tags-plant/table/tag-table.tsx
@@ -1,4 +1,3 @@
-// components/vendor-data-plant/tags-table.tsx
"use client"
import * as React from "react"
@@ -7,177 +6,40 @@ import type {
DataTableFilterField,
DataTableRowAction,
} from "@/types/table"
-import { useRouter } from "next/navigation"
-import { toast } from "sonner"
-import { Trash2, Download, Upload, Loader2, RefreshCcw, Plus } from "lucide-react"
-import ExcelJS from "exceljs"
-import type { Table as TanstackTable } from "@tanstack/react-table"
-import { ClientDataTable } from "@/components/client-data-table/data-table"
+import { toSentenceCase } from "@/lib/utils"
+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 { getColumns } from "./tag-table-column"
import { Tag } from "@/db/schema/vendorData"
import { DeleteTagsDialog } from "./delete-tags-dialog"
+import { TagsTableToolbarActions } from "./tags-table-toolbar-actions"
+import { TagsTableFloatingBar } from "./tags-table-floating-bar"
+import { getTags } from "../service"
import { UpdateTagSheet } from "./update-tag-sheet"
-import { AddTagDialog } from "./add-tag-dialog"
import { useAtomValue } from 'jotai'
import { selectedModeAtom } from '@/atoms'
-import { Skeleton } from "@/components/ui/skeleton"
-import type { ColumnDef } from "@tanstack/react-table"
-import { createDynamicAttributeColumns } from "../column-builder.service"
-import { getAllTagsPlant, getUniqueAttributeKeys } from "../queries"
-import { Button } from "@/components/ui/button"
-import { exportTagsToExcel } from "./tags-export"
-import {
- bulkCreateTags,
- getClassOptions,
- getProjectIdFromContractItemId,
- getSubfieldsByTagType
-} from "../service"
-import { decryptWithServerAction } from "@/components/drm/drmUtils"
+// 여기서 받은 `promises`로부터 태그 목록을 가져와 상태를 세팅
+// 예: "selectedPackageId"는 props로 전달
interface TagsTableProps {
- projectCode: string
- packageCode: string
-}
-
-// 태그 넘버링 룰 인터페이스 (Import용)
-interface TagNumberingRule {
- attributesId: string;
- attributesDescription: string;
- expression: string | null;
- delimiter: string | null;
- sortOrder: number;
-}
-
-interface ClassOption {
- code: string;
- label: string;
- tagTypeCode: string;
- tagTypeDescription: string;
-}
-
-interface SubFieldDef {
- name: string;
- label: string;
- type: "select" | "text";
- options?: { value: string; label: string }[];
- expression?: string;
- delimiter?: string;
+ promises: Promise< [ Awaited<ReturnType<typeof getTags>> ] >
+ selectedPackageId: number
}
-export function TagsTable({
- projectCode,
- packageCode,
-}: TagsTableProps) {
- const router = useRouter()
+export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
+ // 1) 데이터를 가져옴 (server component -> use(...) pattern)
+ const [{ data, pageCount }] = React.use(promises)
const selectedMode = useAtomValue(selectedModeAtom)
- // 상태 관리
- const [tableData, setTableData] = React.useState<Tag[]>([])
- const [columns, setColumns] = React.useState<ColumnDef<Tag>[]>([])
- const [isLoading, setIsLoading] = React.useState(true)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<Tag> | null>(null)
-
- // 선택된 행 관리
- const [selectedRowsData, setSelectedRowsData] = React.useState<Tag[]>([])
- const [clearSelection, setClearSelection] = React.useState(false)
-
- // 다이얼로그 상태
- const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
- const [deleteTarget, setDeleteTarget] = React.useState<Tag[]>([])
- const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false)
-
- // Import/Export 상태
- const [isPending, setIsPending] = React.useState(false)
- const [isExporting, setIsExporting] = React.useState(false)
- const fileInputRef = React.useRef<HTMLInputElement>(null)
-
- // Sync 상태
- const [isSyncing, setIsSyncing] = React.useState(false)
- const [syncId, setSyncId] = React.useState<string | null>(null)
- const pollingRef = React.useRef<NodeJS.Timeout | null>(null)
-
- // Table ref for export
- const tableRef = React.useRef<TanstackTable<Tag> | null>(null)
-
- // Cache for validation
- const [classOptions, setClassOptions] = React.useState<ClassOption[]>([])
- const [subfieldCache, setSubfieldCache] = React.useState<Record<string, SubFieldDef[]>>({})
- const [projectId, setProjectId] = React.useState<number | null>(null)
- // Load project ID
- React.useEffect(() => {
- const fetchProjectId = async () => {
- if (packageCode && projectCode) {
- try {
- const pid = await getProjectIdFromContractItemId(projectCode)
- setProjectId(pid)
- } catch (error) {
- console.error("Failed to fetch project ID:", error)
- }
- }
- }
- fetchProjectId()
- }, [projectCode])
-
- // Load class options
- React.useEffect(() => {
- const loadClassOptions = async () => {
- try {
- const options = await getClassOptions(packageCode, projectCode)
- setClassOptions(options)
- } catch (error) {
- console.error("Failed to load class options:", error)
- }
- }
- loadClassOptions()
- }, [packageCode, projectCode])
-
- // 데이터 및 컬럼 로드
- React.useEffect(() => {
- async function loadTableData() {
- try {
- setIsLoading(true)
-
- const [tagsData, attributeKeys] = await Promise.all([
- getAllTagsPlant(projectCode, packageCode),
- getUniqueAttributeKeys(projectCode, packageCode),
- ])
-
- const baseColumns = getColumns({
- setRowAction,
- onDeleteClick: handleDeleteRow
- })
-
- let dynamicColumns: ColumnDef<Tag>[] = []
- if (attributeKeys.length > 0) {
- dynamicColumns = createDynamicAttributeColumns(attributeKeys)
- }
-
- const actionsColumn = baseColumns.pop()
- const finalColumns = [
- ...baseColumns,
- ...dynamicColumns,
- actionsColumn
- ].filter(Boolean) as ColumnDef<Tag>[]
-
- setTableData(tagsData)
- setColumns(finalColumns)
- } catch (error) {
- console.error("Error loading table data:", error)
- toast.error("Failed to load table data")
- setTableData([])
- setColumns(getColumns({
- setRowAction,
- onDeleteClick: handleDeleteRow
- }))
- } finally {
- setIsLoading(false)
- }
- }
-
- loadTableData()
- }, [projectCode, packageCode])
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
// Filter fields
const filterFields: DataTableFilterField<Tag>[] = [
@@ -205,11 +67,6 @@ export function TagsTable({
type: "text",
},
{
- id: "class",
- label: "Class",
- type: "text",
- },
- {
id: "createdAt",
label: "Created at",
type: "date",
@@ -221,562 +78,78 @@ export function TagsTable({
},
]
- // 선택된 행 개수
- const selectedRowCount = React.useMemo(() => {
- return selectedRowsData.length
- }, [selectedRowsData])
-
- // 개별 행 삭제
- const handleDeleteRow = React.useCallback((rowData: Tag) => {
- setDeleteTarget([rowData])
- setDeleteDialogOpen(true)
- }, [])
-
- // 배치 삭제
- const handleBatchDelete = React.useCallback(() => {
- if (selectedRowsData.length === 0) {
- toast.error("삭제할 항목을 선택해주세요.")
- return
- }
- setDeleteTarget(selectedRowsData)
- setDeleteDialogOpen(true)
- }, [selectedRowsData])
-
- // 삭제 성공 후 처리
- const handleDeleteSuccess = React.useCallback(() => {
- const tagNosToDelete = deleteTarget
- .map(item => item.tagNo)
- .filter(Boolean)
-
- setTableData(prev =>
- prev.filter(item => !tagNosToDelete.includes(item.tagNo))
- )
-
- setSelectedRowsData([])
- setClearSelection(prev => !prev)
- setDeleteTarget([])
-
- toast.success("삭제되었습니다.")
- }, [deleteTarget])
-
- // 클래스 라벨로 태그 타입 코드 찾기
- const getTagTypeCodeByClassLabel = React.useCallback((classLabel: string): string | null => {
- const classOption = classOptions.find(opt => opt.label === classLabel)
- return classOption?.tagTypeCode || null
- }, [classOptions])
-
- // 태그 타입에 따른 서브필드 가져오기
- const fetchSubfieldsByTagType = React.useCallback(async (tagTypeCode: string): Promise<SubFieldDef[]> => {
- if (subfieldCache[tagTypeCode]) {
- return subfieldCache[tagTypeCode]
- }
-
- try {
- const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, "", "")
- const formattedSubFields: SubFieldDef[] = subFields.map(field => ({
- name: field.name,
- label: field.label,
- type: field.type,
- options: field.options || [],
- expression: field.expression ?? undefined,
- delimiter: field.delimiter ?? undefined,
- }))
-
- setSubfieldCache(prev => ({
- ...prev,
- [tagTypeCode]: formattedSubFields
- }))
-
- return formattedSubFields
- } catch (error) {
- console.error(`Error fetching subfields for tagType ${tagTypeCode}:`, error)
- return []
- }
- }, [subfieldCache, projectCode])
-
- // Class 기반 태그 번호 형식 검증
- const validateTagNumberByClass = React.useCallback(async (
- tagNo: string,
- classLabel: string
- ): Promise<string> => {
- if (!tagNo) return "Tag number is empty."
- if (!classLabel) return "Class is empty."
-
- try {
- const tagTypeCode = getTagTypeCodeByClassLabel(classLabel)
- if (!tagTypeCode) {
- return `No tag type found for class '${classLabel}'.`
- }
-
- const subfields = await fetchSubfieldsByTagType(tagTypeCode)
- if (!subfields || subfields.length === 0) {
- return `No subfields found for tag type code '${tagTypeCode}'.`
- }
-
- let remainingTagNo = tagNo
-
- for (const field of subfields) {
- const delimiter = field.delimiter || ""
- let nextDelimiterPos
-
- if (delimiter && remainingTagNo.includes(delimiter)) {
- nextDelimiterPos = remainingTagNo.indexOf(delimiter)
- } else {
- nextDelimiterPos = remainingTagNo.length
- }
-
- const part = remainingTagNo.substring(0, nextDelimiterPos)
-
- if (!part) {
- return `Empty part for field '${field.label}'.`
- }
-
- if (field.expression) {
- try {
- let cleanPattern = field.expression.replace(/^\^/, '').replace(/\$$/, '')
- const regex = new RegExp(`^${cleanPattern}$`)
-
- if (!regex.test(part)) {
- return `Part '${part}' for field '${field.label}' does not match the pattern '${field.expression}'.`
- }
- } catch (error) {
- console.error(`Invalid regex pattern: ${field.expression}`, error)
- return `Invalid pattern for field '${field.label}': ${field.expression}`
- }
- }
-
- if (field.type === "select" && field.options && field.options.length > 0) {
- const validValues = field.options.map(opt => opt.value)
- if (!validValues.includes(part)) {
- return `'${part}' is not a valid value for field '${field.label}'. Valid options: ${validValues.join(", ")}.`
- }
- }
-
- if (delimiter && nextDelimiterPos < remainingTagNo.length) {
- remainingTagNo = remainingTagNo.substring(nextDelimiterPos + delimiter.length)
- } else {
- remainingTagNo = ""
- break
- }
- }
-
- if (remainingTagNo) {
- return `Tag number has extra parts: '${remainingTagNo}'.`
- }
-
- return ""
- } catch (error) {
- console.error("Error validating tag number by class:", error)
- return "Error validating tag number format."
- }
- }, [getTagTypeCodeByClassLabel, fetchSubfieldsByTagType])
-
- // Import 파일 선택
- const handleImportClick = () => {
- fileInputRef.current?.click()
- }
-
- // Import 파일 처리
- const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- const file = e.target.files?.[0]
- if (!file) return
-
- e.target.value = ""
- setIsPending(true)
-
- try {
- const workbook = new ExcelJS.Workbook()
- const arrayBuffer = await decryptWithServerAction(file)
- await workbook.xlsx.load(arrayBuffer)
-
- const worksheet = workbook.worksheets[0]
- const lastColIndex = worksheet.columnCount + 1
- worksheet.getRow(1).getCell(lastColIndex).value = "Error"
-
- const headerRowValues = worksheet.getRow(1).values as ExcelJS.CellValue[]
-
- // Excel header to accessor mapping
- const excelHeaderToAccessor: Record<string, string> = {}
- for (const col of columns) {
- const meta = col.meta as { excelHeader?: string } | undefined
- if (meta?.excelHeader) {
- const accessor = col.id as string
- excelHeaderToAccessor[meta.excelHeader] = accessor
- }
- }
-
- const accessorIndexMap: Record<string, number> = {}
- for (let i = 1; i < headerRowValues.length; i++) {
- const cellVal = String(headerRowValues[i] ?? "").trim()
- if (!cellVal) continue
- const accessor = excelHeaderToAccessor[cellVal]
- if (accessor) {
- accessorIndexMap[accessor] = i
- }
- }
-
- let errorCount = 0
- const importedRows: Tag[] = []
- const fileTagNos = new Set<string>()
- const lastRow = worksheet.lastRow?.number || 1
-
- for (let rowNum = 2; rowNum <= lastRow; rowNum++) {
- const row = worksheet.getRow(rowNum)
- const rowVals = row.values as ExcelJS.CellValue[]
- if (!rowVals || rowVals.length <= 1) continue
-
- let errorMsg = ""
-
- const tagNoIndex = accessorIndexMap["tagNo"]
- const classIndex = accessorIndexMap["class"]
-
- const tagNo = tagNoIndex ? String(rowVals[tagNoIndex] ?? "").trim() : ""
- const classVal = classIndex ? String(rowVals[classIndex] ?? "").trim() : ""
-
- if (!tagNo) {
- errorMsg += `Tag No is empty. `
- }
- if (!classVal) {
- errorMsg += `Class is empty. `
- }
-
- if (tagNo) {
- const dup = tableData.find(t => t.tagNo === tagNo)
- if (dup) {
- errorMsg += `TagNo '${tagNo}' already exists. `
- }
-
- if (fileTagNos.has(tagNo)) {
- errorMsg += `TagNo '${tagNo}' is duplicated within this file. `
- } else {
- fileTagNos.add(tagNo)
- }
- }
-
- if (tagNo && classVal && !errorMsg) {
- const classValidationError = await validateTagNumberByClass(tagNo, classVal)
- if (classValidationError) {
- errorMsg += classValidationError + " "
- }
- }
-
- if (errorMsg) {
- row.getCell(lastColIndex).value = errorMsg.trim()
- errorCount++
- } else {
- const finalTagType = getTagTypeCodeByClassLabel(classVal) ?? ""
-
- importedRows.push({
- id: 0,
- packageCode: packageCode,
- projectCode: projectCode,
- formId: null,
- tagNo,
- tagType: finalTagType,
- class: classVal,
- description: String(rowVals[accessorIndexMap["description"] ?? 0] ?? "").trim(),
- createdAt: new Date(),
- updatedAt: new Date(),
- })
- }
- }
-
- if (errorCount > 0) {
- const outBuf = await workbook.xlsx.writeBuffer()
- const errorFile = new Blob([outBuf])
- const url = URL.createObjectURL(errorFile)
- const link = document.createElement("a")
- link.href = url
- link.download = "tag_import_errors.xlsx"
- link.click()
- URL.revokeObjectURL(url)
-
- toast.error(`There are ${errorCount} error row(s). Please see downloaded file.`)
- return
- }
-
- if (importedRows.length > 0) {
- const result = await bulkCreateTags(importedRows, projectCode, packageCode)
- if ("error" in result) {
- toast.error(result.error)
- } else {
- toast.success(`${result.data.createdCount}개의 태그가 성공적으로 생성되었습니다.`)
- router.refresh()
- }
- }
- } catch (err) {
- console.error(err)
- toast.error("파일 업로드 중 오류가 발생했습니다.")
- } finally {
- setIsPending(false)
- }
- }
-
- // Export 함수
- const handleExport = async () => {
- if (!tableRef.current) {
- toast.error("테이블이 준비되지 않았습니다.")
- return
- }
-
- try {
- setIsExporting(true)
- await exportTagsToExcel(tableRef.current, packageCode, projectCode, {
- filename: `Tags_${packageCode}_${projectCode}`,
- excludeColumns: ["select", "actions", "createdAt", "updatedAt"],
- })
- toast.success("태그 목록이 성공적으로 내보내졌습니다.")
- } catch (error) {
- console.error("Export error:", error)
- toast.error("태그 목록 내보내기 중 오류가 발생했습니다.")
- } finally {
- setIsExporting(false)
- }
- }
-
- // Sync 함수
- const startGetTags = async () => {
- try {
- setIsSyncing(true)
-
- const response = await fetch('/api/cron/tags-plant/start', {
- method: 'POST',
- body: JSON.stringify({
- projectCode: projectCode,
- packageCode: packageCode,
- mode: selectedMode
- })
- })
-
- if (!response.ok) {
- const errorData = await response.json()
- throw new Error(errorData.error || 'Failed to start tag import')
- }
-
- const data = await response.json()
-
- if (data.syncId) {
- setSyncId(data.syncId)
- toast.info('Tag import started. This may take a while...')
- startPolling(data.syncId)
- } else {
- throw new Error('No import ID returned from server')
- }
- } catch (error) {
- console.error('Error starting tag import:', error)
- toast.error(
- error instanceof Error
- ? error.message
- : 'An error occurred while starting tag import'
- )
- setIsSyncing(false)
- }
- }
-
- const startPolling = (id: string) => {
- if (pollingRef.current) {
- clearInterval(pollingRef.current)
- }
-
- pollingRef.current = setInterval(async () => {
- try {
- const response = await fetch(`/api/cron/tags-plant/status?id=${id}`)
-
- if (!response.ok) {
- throw new Error('Failed to get tag import status')
- }
-
- const data = await response.json()
-
- if (data.status === 'completed') {
- if (pollingRef.current) {
- clearInterval(pollingRef.current)
- pollingRef.current = null
- }
-
- router.refresh()
- setIsSyncing(false)
- setSyncId(null)
+ // 3) useDataTable 훅으로 react-table 구성
+ const { table } = useDataTable({
+ data: data, // <-- 여기서 tableData 사용
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ // sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ columnResizeMode: "onEnd",
- toast.success(
- `Tags imported successfully! ${data.result?.processedCount || 0} items processed.`
- )
- } else if (data.status === 'failed') {
- if (pollingRef.current) {
- clearInterval(pollingRef.current)
- pollingRef.current = null
- }
+ })
- setIsSyncing(false)
- setSyncId(null)
- toast.error(data.error || 'Import failed')
- }
- } catch (error) {
- console.error('Error checking importing status:', error)
- }
- }, 5000)
- }
+ const [isCompact, setIsCompact] = React.useState<boolean>(false)
- // rowAction 처리
- React.useEffect(() => {
- if (rowAction?.type === "delete") {
- handleDeleteRow(rowAction.row.original)
- setRowAction(null)
- }
- }, [rowAction, handleDeleteRow])
- // Cleanup
- React.useEffect(() => {
- return () => {
- if (pollingRef.current) {
- clearInterval(pollingRef.current)
- }
- }
+ const handleCompactChange = React.useCallback((compact: boolean) => {
+ setIsCompact(compact)
}, [])
-
- // 로딩 중
- if (isLoading) {
- return (
- <div className="space-y-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-[500px] w-full" />
- <Skeleton className="h-10 w-full" />
- </div>
- )
- }
-
+
+
return (
<>
- <ClientDataTable
- data={tableData}
- columns={columns}
- advancedFilterFields={advancedFilterFields}
- autoSizeColumns
- onSelectedRowsChange={setSelectedRowsData}
- clearSelection={clearSelection}
- onTableReady={(table) => {
- tableRef.current = table
- }}
- >
- <div className="flex items-center gap-2">
- {/* 삭제 버튼 - 선택된 항목이 있을 때만 */}
- {selectedRowCount > 0 && (
- <Button
- variant="destructive"
- size="sm"
- onClick={handleBatchDelete}
- >
- <Trash2 className="mr-2 size-4" />
- Delete ({selectedRowCount})
- </Button>
- )}
-
- {/* Get Tags 버튼 */}
- <Button
- variant="samsung"
- size="sm"
- onClick={startGetTags}
- disabled={isSyncing}
- >
- <RefreshCcw className={`size-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} />
- <span className="hidden sm:inline">
- {isSyncing ? 'Syncing...' : 'Get Tags'}
- </span>
- </Button>
-
- {/* Add Tag 버튼 */}
- <AddTagDialog
- projectCode={projectCode}
- packageCode={packageCode}/>
+ <DataTable
+ table={table}
+ compact={isCompact}
- {/* Import 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleImportClick}
- disabled={isPending || isExporting}
- >
- {isPending ? (
- <Loader2 className="size-4 mr-2 animate-spin" />
- ) : (
- <Upload className="size-4 mr-2" />
- )}
- <span className="hidden sm:inline">Import</span>
- </Button>
-
- {/* Export 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleExport}
- disabled={isPending || isExporting || !tableRef.current}
- >
- {isExporting ? (
- <Loader2 className="size-4 mr-2 animate-spin" />
- ) : (
- <Download className="size-4 mr-2" />
- )}
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
- </ClientDataTable>
-
- {/* Hidden file input */}
- <input
- ref={fileInputRef}
- type="file"
- accept=".xlsx,.xls"
- className="hidden"
- onChange={handleFileChange}
- />
+ floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>}
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ enableCompactToggle={true}
+ compactStorageKey="tagTableCompact"
+ onCompactChange={handleCompactChange}
+ >
+ {/*
+ 4) ToolbarActions에 tableData, setTableData 넘겨서
+ import 시 상태 병합
+ */}
+ <TagsTableToolbarActions
+ table={table}
+ selectedPackageId={selectedPackageId}
+ tableData={data} // <-- pass current data
+ selectedMode={selectedMode}
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
- {/* Update Sheet */}
<UpdateTagSheet
open={rowAction?.type === "update"}
- onOpenChange={(open) => {
- if (!open) setRowAction(null)
- }}
+ onOpenChange={() => setRowAction(null)}
tag={rowAction?.row.original ?? null}
- packageCode={packageCode}
- projectCode={projectCode}
- onUpdateSuccess={(updatedValues) => {
- if (rowAction?.row.original?.tagNo) {
- const tagNo = rowAction.row.original.tagNo
- setTableData(prev =>
- prev.map(item =>
- item.tagNo === tagNo ? updatedValues : item
- )
- )
- }
- }}
+ selectedPackageId={selectedPackageId}
/>
- {/* Delete Dialog */}
+
<DeleteTagsDialog
- tags={deleteTarget}
- packageCode={packageCode}
- projectCode={projectCode}
- open={deleteDialogOpen}
- onOpenChange={(open) => {
- if (!open) {
- setDeleteDialogOpen(false)
- setDeleteTarget([])
- }
- }}
- onSuccess={handleDeleteSuccess}
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ tags={rowAction?.row.original ? [rowAction?.row.original] : []}
showTrigger={false}
+ onSuccess={() => rowAction?.row.toggleSelected(false)}
+ selectedPackageId={selectedPackageId}
/>
-
- {/* Add Tag Dialog */}
- {/* <AddTagDialog
- projectCode={projectCode}
- packageCode={packageCode}
- open={addTagDialogOpen}
- onOpenChange={setAddTagDialogOpen}
- onSuccess={() => {
- router.refresh()
- }}
- /> */}
</>
)
} \ No newline at end of file
diff --git a/lib/tags-plant/table/tags-export.tsx b/lib/tags-plant/table/tags-export.tsx
index a3255a0b..fa85148d 100644
--- a/lib/tags-plant/table/tags-export.tsx
+++ b/lib/tags-plant/table/tags-export.tsx
@@ -15,8 +15,7 @@ import { getClassOptions } from "../service"
*/
export async function exportTagsToExcel(
table: Table<Tag>,
- packageCode: string,
- projectCode: string,
+ selectedPackageId: number,
{
filename = "Tags",
excludeColumns = ["select", "actions", "createdAt", "updatedAt"],
@@ -43,7 +42,7 @@ export async function exportTagsToExcel(
const worksheet = workbook.addWorksheet("Tags")
// 3. Tag Class 옵션 가져오기
- const classOptions = await getClassOptions(packageCode, projectCode)
+ const classOptions = await getClassOptions(selectedPackageId)
// 4. 유효성 검사 시트 생성
const validationSheet = workbook.addWorksheet("ValidationData")
diff --git a/lib/tags-plant/table/tags-table-floating-bar.tsx b/lib/tags-plant/table/tags-table-floating-bar.tsx
index eadbfb12..8d55b7ac 100644
--- a/lib/tags-plant/table/tags-table-floating-bar.tsx
+++ b/lib/tags-plant/table/tags-table-floating-bar.tsx
@@ -36,13 +36,12 @@ import { Tag } from "@/db/schema/vendorData"
interface TagsTableFloatingBarProps {
table: Table<Tag>
- packageCode: string
- projectCode: string
+ selectedPackageId: number
}
-export function TagsTableFloatingBar({ table, packageCode, projectCode}: TagsTableFloatingBarProps) {
+export function TagsTableFloatingBar({ table, selectedPackageId }: TagsTableFloatingBarProps) {
const rows = table.getFilteredSelectedRowModel().rows
const [isPending, startTransition] = React.useTransition()
diff --git a/lib/tags-plant/table/tags-table-toolbar-actions.tsx b/lib/tags-plant/table/tags-table-toolbar-actions.tsx
index c80a600e..cc2d82b4 100644
--- a/lib/tags-plant/table/tags-table-toolbar-actions.tsx
+++ b/lib/tags-plant/table/tags-table-toolbar-actions.tsx
@@ -52,8 +52,7 @@ interface TagsTableToolbarActionsProps {
/** react-table 객체 */
table: Table<Tag>
/** 현재 선택된 패키지 ID */
- packageCode: string
- projectCode: string
+ selectedPackageId: number
/** 현재 태그 목록(상태) */
tableData: Tag[]
/** 태그 목록을 갱신하는 setState */
@@ -69,8 +68,7 @@ interface TagsTableToolbarActionsProps {
*/
export function TagsTableToolbarActions({
table,
- packageCode,
- projectCode,
+ selectedPackageId,
tableData,
selectedMode
}: TagsTableToolbarActionsProps) {
@@ -96,7 +94,7 @@ export function TagsTableToolbarActions({
React.useEffect(() => {
const loadClassOptions = async () => {
try {
- const options = await getClassOptions(packageCode, projectCode)
+ const options = await getClassOptions(selectedPackageId)
setClassOptions(options)
} catch (error) {
console.error("Failed to load class options:", error)
@@ -104,7 +102,7 @@ export function TagsTableToolbarActions({
}
loadClassOptions()
- }, [packageCode, projectCode])
+ }, [selectedPackageId])
// 숨겨진 <input>을 클릭
function handleImportClick() {
@@ -137,11 +135,12 @@ export function TagsTableToolbarActions({
const [projectId, setProjectId] = React.useState<number | null>(null);
+ // Add useEffect to fetch projectId when selectedPackageId changes
React.useEffect(() => {
const fetchProjectId = async () => {
- if (packageCode && projectCode) {
+ if (selectedPackageId) {
try {
- const pid = await getProjectIdFromContractItemId(projectCode );
+ const pid = await getProjectIdFromContractItemId(selectedPackageId);
setProjectId(pid);
} catch (error) {
console.error("Failed to fetch project ID:", error);
@@ -151,7 +150,7 @@ export function TagsTableToolbarActions({
};
fetchProjectId();
- }, [projectCode]);
+ }, [selectedPackageId]);
// 특정 attributesId에 대한 옵션 가져오기
const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => {
@@ -196,7 +195,7 @@ export function TagsTableToolbarActions({
}
try {
- const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode)
+ const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId)
// API 응답을 SubFieldDef 형식으로 변환
const formattedSubFields: SubFieldDef[] = subFields.map(field => ({
@@ -479,7 +478,7 @@ export function TagsTableToolbarActions({
if (tagNo) {
// 이미 tableData 내 존재 여부
const dup = tableData.find(
- (t) => t.tagNo === tagNo
+ (t) => t.contractItemId === selectedPackageId && t.tagNo === tagNo
)
if (dup) {
errorMsg += `TagNo '${tagNo}' already exists. `
@@ -524,8 +523,7 @@ export function TagsTableToolbarActions({
// 정상 행을 importedRows에 추가
importedRows.push({
id: 0, // 임시
- packageCode: packageCode,
- projectCode: projectCode,
+ contractItemId: selectedPackageId,
formId: null,
tagNo,
tagType: finalTagType, // ← 코드로 저장할지, Description으로 저장할지 결정
@@ -554,7 +552,7 @@ export function TagsTableToolbarActions({
// 정상 행이 있으면 태그 생성 요청
if (importedRows.length > 0) {
- const result = await bulkCreateTags(importedRows, projectCode, packageCode);
+ const result = await bulkCreateTags(importedRows, selectedPackageId);
if ("error" in result) {
toast.error(result.error);
} else {
@@ -577,8 +575,8 @@ export function TagsTableToolbarActions({
setIsExporting(true)
// 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출
- await exportTagsToExcel(table, packageCode,projectCode, {
- filename: `Tags_${packageCode}_${projectCode}`,
+ await exportTagsToExcel(table, selectedPackageId, {
+ filename: `Tags_${selectedPackageId}`,
excludeColumns: ["select", "actions", "createdAt", "updatedAt"],
})
@@ -596,11 +594,10 @@ export function TagsTableToolbarActions({
setIsLoading(true)
// API 엔드포인트 호출 - 작업 시작만 요청
- const response = await fetch('/api/cron/tags-plant/start', {
+ const response = await fetch('/api/cron/tags/start', {
method: 'POST',
body: JSON.stringify({
- projectCode: projectCode,
- packageCode: packageCode,
+ packageId: selectedPackageId,
mode: selectedMode // 모드 정보 추가
})
})
@@ -641,7 +638,7 @@ export function TagsTableToolbarActions({
// 5초마다 상태 확인
pollingRef.current = setInterval(async () => {
try {
- const response = await fetch(`/api/cron/tags-plant/status?id=${id}`)
+ const response = await fetch(`/api/cron/tags/status?id=${id}`)
if (!response.ok) {
throw new Error('Failed to get tag import status')
@@ -702,8 +699,7 @@ export function TagsTableToolbarActions({
.getFilteredSelectedRowModel()
.rows.map((row) => row.original)}
onSuccess={() => table.toggleAllRowsSelected(false)}
- projectCode={projectCode}
- packageCode={packageCode}
+ selectedPackageId={selectedPackageId}
/>
) : null}
<Button
@@ -719,7 +715,7 @@ export function TagsTableToolbarActions({
</span>
</Button>
- <AddTagDialog projectCode={projectCode} packageCode={packageCode} />
+ <AddTagDialog selectedPackageId={selectedPackageId} />
{/* Import */}
<Button
diff --git a/lib/tags-plant/table/update-tag-sheet.tsx b/lib/tags-plant/table/update-tag-sheet.tsx
index 2be1e732..613abaa9 100644
--- a/lib/tags-plant/table/update-tag-sheet.tsx
+++ b/lib/tags-plant/table/update-tag-sheet.tsx
@@ -50,7 +50,7 @@ import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import { Tag } from "@/db/schema/vendorData"
-import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags-plant/service"
+import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags/service"
// SubFieldDef 인터페이스
interface SubFieldDef {
@@ -84,11 +84,10 @@ type UpdateTagSchema = z.infer<typeof updateTagSchema> & Record<string, string>
interface UpdateTagSheetProps extends React.ComponentPropsWithRef<typeof Sheet> {
tag: Tag | null
- packageCode: string
- projectCode: string
+ selectedPackageId: number
}
-export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: UpdateTagSheetProps) {
+export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSheetProps) {
const [isUpdatePending, startUpdateTransition] = React.useTransition()
const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([])
const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null)
@@ -111,7 +110,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat
setIsLoadingClasses(true)
try {
- const result = await getClassOptions(packageCode, projectCode)
+ const result = await getClassOptions(selectedPackageId)
setClassOptions(result)
} catch (err) {
toast.error("클래스 옵션을 불러오는데 실패했습니다.")
@@ -165,7 +164,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat
async function loadSubFieldsByTagTypeCode(tagTypeCode: string) {
setIsLoadingSubFields(true)
try {
- const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode)
+ const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId)
const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({
name: field.name,
label: field.label,
@@ -222,7 +221,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat
),
}
- const result = await updateTag(tagData, projectCode,packageCode )
+ const result = await updateTag(tagData, selectedPackageId)
if ("error" in result) {
toast.error(result.error)
diff --git a/lib/vendor-data/services.ts b/lib/vendor-data/services.ts
index fe4e56ae..8c8b21d2 100644
--- a/lib/vendor-data/services.ts
+++ b/lib/vendor-data/services.ts
@@ -62,8 +62,6 @@ export async function getVendorProjectsAndContracts(
itemId: contractItems.id,
itemName: items.itemName,
- packageCode: items.packageCode,
- packageName: items.description,
})
.from(contracts)
.innerJoin(projects, eq(contracts.projectId, projects.id))
@@ -128,94 +126,3 @@ export async function getVendorProjectsAndContracts(
return Array.from(projectMap.values())
}
-interface ProjectWithPackages {
- projectId: number
- projectCode: string
- projectName: string
- projectType: "ship" | "plant"
- packages: {
- packageCode: string
- packageName: string | null
- }[]
-}
-
-export async function getVendorProjectsWithPackages(
- vendorId?: number,
- projectType?: "ship" | "plant"
-): Promise<ProjectWithPackages[]> {
- // 세션에서 도메인 정보 가져오기
- const session = await getServerSession(authOptions)
-
- // EVCP 도메인일 때만 전체 조회
- const isEvcpDomain = session?.user?.domain === "evcp"
-
- // where 조건들을 배열로 관리
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const whereConditions: any[] = []
-
- // vendorId 조건 추가
- if (!isEvcpDomain && vendorId) {
- whereConditions.push(eq(contracts.vendorId, vendorId))
- }
-
- // projectType 조건 추가
- if (projectType) {
- whereConditions.push(eq(projects.type, projectType))
- }
-
- const query = db
- .select({
- projectId: projects.id,
- projectCode: projects.code,
- projectName: projects.name,
- projectType: projects.type,
-
- packageCode: items.packageCode,
- packageName: items.description,
- })
- .from(contracts)
- .innerJoin(projects, eq(contracts.projectId, projects.id))
- .innerJoin(contractItems, eq(contractItems.contractId, contracts.id))
- .innerJoin(items, eq(contractItems.itemId, items.id))
-
- // 조건이 있으면 where 절 추가
- if (whereConditions.length > 0) {
- query.where(and(...whereConditions))
- }
-
- const rows = await query
-
- const projectMap = new Map<number, ProjectWithPackages>()
-
- for (const row of rows) {
- // 1) 프로젝트 그룹 찾기
- let projectEntry = projectMap.get(row.projectId)
- if (!projectEntry) {
- // 새 프로젝트 항목 생성
- projectEntry = {
- projectId: row.projectId,
- projectCode: row.projectCode,
- projectName: row.projectName,
- projectType: row.projectType,
- packages: [],
- }
- projectMap.set(row.projectId, projectEntry)
- }
-
- // 2) 프로젝트의 packages 배열에 패키지 추가 (중복 체크)
- // packageCode가 같은 항목이 이미 존재하는지 확인
- const existingPackage = projectEntry.packages.find(
- (pkg) => pkg.packageCode === row.packageCode
- )
-
- // 같은 packageCode가 없는 경우에만 추가
- if (!existingPackage) {
- projectEntry.packages.push({
- packageCode: row.packageCode,
- packageName: row.packageName,
- })
- }
- }
-
- return Array.from(projectMap.values())
-} \ No newline at end of file
diff --git a/lib/vendor-document/service.ts b/lib/vendor-document/service.ts
index 48e3fa3f..bf2b0b7a 100644
--- a/lib/vendor-document/service.ts
+++ b/lib/vendor-document/service.ts
@@ -2,14 +2,14 @@
import { eq, SQL } from "drizzle-orm"
import db from "@/db/db"
-import { stageSubmissions, stageDocuments, stageIssueStages,documentAttachments, documents, issueStages, revisions, stageDocumentsView,vendorDocumentsView ,stageSubmissionAttachments, StageIssueStage, StageDocumentsView, StageDocument,} from "@/db/schema/vendorDocu"
+import { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu"
import { GetVendorDcoumentsSchema } from "./validations"
import { unstable_cache } from "@/lib/unstable-cache";
import { filterColumns } from "@/lib/filter-columns";
import { getErrorMessage } from "@/lib/handle-error";
import { asc, desc, ilike, inArray, and, gte, lte, not, or , isNotNull, isNull} from "drizzle-orm";
import { countVendorDocuments, selectVendorDocuments } from "./repository"
-import { contractItems, projects, items,contracts } from "@/db/schema"
+import { contractItems } from "@/db/schema"
import { saveFile } from "../file-stroage"
import path from "path"
@@ -494,706 +494,4 @@ export async function fetchRevisionsByStageParams(
console.error("Error fetching revisions:", error);
return [];
}
-}
-
-// 타입 정의
-type SubmissionInfo = {
- id: number;
- revisionNumber: number;
- revisionCode: string;
- revisionType: string;
- submissionStatus: string;
- submittedBy: string;
- submittedAt: Date;
- reviewStatus: string | null;
- buyerSystemStatus: string | null;
- syncStatus: string;
-};
-
-type AttachmentInfo = {
- id: number;
- fileName: string;
- originalFileName: string;
- fileSize: number;
- fileType: string | null;
- storageUrl: string | null;
- syncStatus: string;
- buyerSystemStatus: string | null;
- uploadedAt: Date;
-};
-
-// Server Action: Fetch documents by projectCode and packageCode
-export async function fetchDocumentsByProjectAndPackage(
- projectCode: string,
- packageCode: string
-): Promise<StageDocument[]> {
- try {
- // First, find the project by code
- const projectResult = await db
- .select({ id: projects.id })
- .from(projects)
- .where(eq(projects.code, projectCode))
- .limit(1);
-
- if (!projectResult.length) {
- return [];
- }
-
- const projectId = projectResult[0].id;
-
- // Find contract through contractItems joined with items table
- const contractItemResult = await db
- .select({
- contractId: contractItems.contractId
- })
- .from(contractItems)
- .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
- .innerJoin(items, eq(contractItems.itemId, items.id))
- .where(
- and(
- eq(contracts.projectId, projectId),
- eq(items.packageCode, packageCode)
- )
- )
- .limit(1);
-
- if (!contractItemResult.length) {
- return [];
- }
-
- const contractId = contractItemResult[0].contractId;
-
- // Get stage documents
- const docsResult = await db
- .select({
- id: stageDocuments.id,
- docNumber: stageDocuments.docNumber,
- title: stageDocuments.title,
- vendorDocNumber: stageDocuments.vendorDocNumber,
- status: stageDocuments.status,
- issuedDate: stageDocuments.issuedDate,
- docClass: stageDocuments.docClass,
- projectId: stageDocuments.projectId,
- vendorId: stageDocuments.vendorId,
- contractId: stageDocuments.contractId,
- buyerSystemStatus: stageDocuments.buyerSystemStatus,
- buyerSystemComment: stageDocuments.buyerSystemComment,
- lastSyncedAt: stageDocuments.lastSyncedAt,
- syncStatus: stageDocuments.syncStatus,
- syncError: stageDocuments.syncError,
- syncVersion: stageDocuments.syncVersion,
- lastModifiedBy: stageDocuments.lastModifiedBy,
- createdAt: stageDocuments.createdAt,
- updatedAt: stageDocuments.updatedAt,
- })
- .from(stageDocuments)
- .where(
- and(
- eq(stageDocuments.projectId, projectId),
- eq(stageDocuments.contractId, contractId),
- eq(stageDocuments.status, "ACTIVE")
- )
- )
- .orderBy(stageDocuments.docNumber);
-
- return docsResult;
- } catch (error) {
- console.error("Error fetching documents:", error);
- return [];
- }
-}
-
-// Server Action: Fetch stages by documentId
-export async function fetchStagesByDocumentIdPlant(
- documentId: number
-): Promise<StageIssueStage[]> {
- try {
- const stagesResult = await db
- .select({
- id: stageIssueStages.id,
- documentId: stageIssueStages.documentId,
- stageName: stageIssueStages.stageName,
- planDate: stageIssueStages.planDate,
- actualDate: stageIssueStages.actualDate,
- stageStatus: stageIssueStages.stageStatus,
- stageOrder: stageIssueStages.stageOrder,
- priority: stageIssueStages.priority,
- assigneeId: stageIssueStages.assigneeId,
- assigneeName: stageIssueStages.assigneeName,
- reminderDays: stageIssueStages.reminderDays,
- description: stageIssueStages.description,
- notes: stageIssueStages.notes,
- createdAt: stageIssueStages.createdAt,
- updatedAt: stageIssueStages.updatedAt,
- })
- .from(stageIssueStages)
- .where(eq(stageIssueStages.documentId, documentId))
- .orderBy(stageIssueStages.stageOrder, stageIssueStages.stageName);
-
- return stagesResult;
- } catch (error) {
- console.error("Error fetching stages:", error);
- return [];
- }
-}
-
-// Server Action: Fetch submissions (revisions) by documentId and stageName
-export async function fetchSubmissionsByStageParams(
- documentId: number,
- stageName: string
-): Promise<SubmissionInfo[]> {
- try {
- // First, find the stageId
- const stageResult = await db
- .select({ id: stageIssueStages.id })
- .from(stageIssueStages)
- .where(
- and(
- eq(stageIssueStages.documentId, documentId),
- eq(stageIssueStages.stageName, stageName)
- )
- )
- .limit(1);
-
- if (!stageResult.length) {
- return [];
- }
-
- const stageId = stageResult[0].id;
-
- // Then, get submissions for this stage
- const submissionsResult = await db
- .select({
- id: stageSubmissions.id,
- revisionNumber: stageSubmissions.revisionNumber,
- revisionCode: stageSubmissions.revisionCode,
- revisionType: stageSubmissions.revisionType,
- submissionStatus: stageSubmissions.submissionStatus,
- submittedBy: stageSubmissions.submittedBy,
- submittedAt: stageSubmissions.submittedAt,
- reviewStatus: stageSubmissions.reviewStatus,
- buyerSystemStatus: stageSubmissions.buyerSystemStatus,
- syncStatus: stageSubmissions.syncStatus,
- })
- .from(stageSubmissions)
- .where(eq(stageSubmissions.stageId, stageId))
- .orderBy(stageSubmissions.revisionNumber);
-
- return submissionsResult;
- } catch (error) {
- console.error("Error fetching submissions:", error);
- return [];
- }
-}
-
-// View를 활용한 더 효율적인 조회
-export async function fetchDocumentsViewByProjectAndPackage(
- projectCode: string,
- packageCode: string
-): Promise<StageDocumentsView[]> {
- try {
- // First, find the project by code
- const projectResult = await db
- .select({ id: projects.id })
- .from(projects)
- .where(eq(projects.code, projectCode))
- .limit(1);
-
- if (!projectResult.length) {
- return [];
- }
-
- const projectId = projectResult[0].id;
-
- // Find contract through contractItems joined with items
- const contractItemResult = await db
- .select({
- contractId: contractItems.contractId
- })
- .from(contractItems)
- .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
- .innerJoin(items, eq(contractItems.itemId, items.id))
- .where(
- and(
- eq(contracts.projectId, projectId),
- eq(items.packageCode, packageCode)
- )
- )
- .limit(1);
-
- if (!contractItemResult.length) {
- return [];
- }
-
- const contractId = contractItemResult[0].contractId;
-
- // Use the view for enriched data (includes progress, current stage, etc.)
- const documentsViewResult = await db
- .select()
- .from(stageDocumentsView)
- .where(
- and(
- eq(stageDocumentsView.projectId, projectId),
- eq(stageDocumentsView.contractId, contractId),
- eq(stageDocumentsView.status, "ACTIVE")
- )
- )
- .orderBy(stageDocumentsView.docNumber);
-
- return documentsViewResult;
- } catch (error) {
- console.error("Error fetching documents view:", error);
- return [];
- }
-}
-
-// Server Action: Fetch submission attachments by submissionId
-export async function fetchAttachmentsBySubmissionId(
- submissionId: number
-): Promise<AttachmentInfo[]> {
- try {
- const attachmentsResult = await db
- .select({
- id: stageSubmissionAttachments.id,
- fileName: stageSubmissionAttachments.fileName,
- originalFileName: stageSubmissionAttachments.originalFileName,
- fileSize: stageSubmissionAttachments.fileSize,
- fileType: stageSubmissionAttachments.fileType,
- storageUrl: stageSubmissionAttachments.storageUrl,
- syncStatus: stageSubmissionAttachments.syncStatus,
- buyerSystemStatus: stageSubmissionAttachments.buyerSystemStatus,
- uploadedAt: stageSubmissionAttachments.uploadedAt,
- })
- .from(stageSubmissionAttachments)
- .where(
- and(
- eq(stageSubmissionAttachments.submissionId, submissionId),
- eq(stageSubmissionAttachments.status, "ACTIVE")
- )
- )
- .orderBy(stageSubmissionAttachments.uploadedAt);
-
- return attachmentsResult;
- } catch (error) {
- console.error("Error fetching attachments:", error);
- return [];
- }
-}
-
-// 추가 헬퍼: 특정 제출의 상세 정보 (첨부파일 포함)
-export async function getSubmissionWithAttachments(submissionId: number) {
- try {
- const [submission] = await db
- .select({
- id: stageSubmissions.id,
- stageId: stageSubmissions.stageId,
- documentId: stageSubmissions.documentId,
- revisionNumber: stageSubmissions.revisionNumber,
- revisionCode: stageSubmissions.revisionCode,
- revisionType: stageSubmissions.revisionType,
- submissionStatus: stageSubmissions.submissionStatus,
- submittedBy: stageSubmissions.submittedBy,
- submittedByEmail: stageSubmissions.submittedByEmail,
- submittedAt: stageSubmissions.submittedAt,
- reviewedBy: stageSubmissions.reviewedBy,
- reviewedAt: stageSubmissions.reviewedAt,
- submissionTitle: stageSubmissions.submissionTitle,
- submissionDescription: stageSubmissions.submissionDescription,
- reviewStatus: stageSubmissions.reviewStatus,
- reviewComments: stageSubmissions.reviewComments,
- vendorId: stageSubmissions.vendorId,
- totalFiles: stageSubmissions.totalFiles,
- buyerSystemStatus: stageSubmissions.buyerSystemStatus,
- syncStatus: stageSubmissions.syncStatus,
- createdAt: stageSubmissions.createdAt,
- updatedAt: stageSubmissions.updatedAt,
- })
- .from(stageSubmissions)
- .where(eq(stageSubmissions.id, submissionId))
- .limit(1);
-
- if (!submission) {
- return null;
- }
-
- const attachments = await fetchAttachmentsBySubmissionId(submissionId);
-
- return {
- ...submission,
- attachments,
- };
- } catch (error) {
- console.error("Error getting submission with attachments:", error);
- return null;
- }
-}
-
-
-interface CreateSubmissionResult {
- success: boolean;
- error?: string;
- submissionId?: number;
-}
-
-export async function createSubmissionAction(
- formData: FormData
-): Promise<CreateSubmissionResult> {
- try {
- // Extract form data
- const documentId = formData.get("documentId") as string;
- const stageName = formData.get("stageName") as string;
- const revisionCode = formData.get("revisionCode") as string;
- const customFileName = formData.get("customFileName") as string;
- const submittedBy = formData.get("submittedBy") as string;
- const submittedByEmail = formData.get("submittedByEmail") as string | null;
- const submissionTitle = formData.get("submissionTitle") as string | null;
- const submissionDescription = formData.get("submissionDescription") as string | null;
- const vendorId = formData.get("vendorId") as string;
- const attachment = formData.get("attachment") as File | null;
-
- // Validate required fields
- if (!documentId || !stageName || !revisionCode || !submittedBy || !vendorId) {
- return {
- success: false,
- error: "Missing required fields",
- };
- }
-
- const parsedDocumentId = parseInt(documentId, 10);
- const parsedVendorId = parseInt(vendorId, 10);
-
- // Validate parsed numbers
- if (isNaN(parsedDocumentId) || isNaN(parsedVendorId)) {
- return {
- success: false,
- error: "Invalid documentId or vendorId",
- };
- }
-
- // Find the document
- const [document] = await db
- .select()
- .from(stageDocuments)
- .where(eq(stageDocuments.id, parsedDocumentId))
- .limit(1);
-
- if (!document) {
- return {
- success: false,
- error: "Document not found",
- };
- }
-
- // Find the stage
- const [stage] = await db
- .select()
- .from(stageIssueStages)
- .where(
- and(
- eq(stageIssueStages.documentId, parsedDocumentId),
- eq(stageIssueStages.stageName, stageName)
- )
- )
- .limit(1);
-
- if (!stage) {
- return {
- success: false,
- error: `Stage "${stageName}" not found for this document`,
- };
- }
-
- const stageId = stage.id;
-
- // Get the latest revision number for this stage
- const existingSubmissions = await db
- .select({
- revisionNumber: stageSubmissions.revisionNumber,
- })
- .from(stageSubmissions)
- .where(eq(stageSubmissions.stageId, stageId))
- .orderBy(desc(stageSubmissions.revisionNumber))
- .limit(1);
-
- const nextRevisionNumber = existingSubmissions.length > 0
- ? existingSubmissions[0].revisionNumber + 1
- : 1;
-
- // Check if revision code already exists for this stage
- const [existingRevisionCode] = await db
- .select()
- .from(stageSubmissions)
- .where(
- and(
- eq(stageSubmissions.stageId, stageId),
- eq(stageSubmissions.revisionCode, revisionCode)
- )
- )
- .limit(1);
-
- if (existingRevisionCode) {
- return {
- success: false,
- error: `Revision code "${revisionCode}" already exists for this stage`,
- };
- }
-
- // Get vendor code from vendors table
- const [vendor] = await db
- .select({ vendorCode: vendors.vendorCode })
- .from(vendors)
- .where(eq(vendors.id, parsedVendorId))
- .limit(1);
-
- const vendorCode = vendor?.vendorCode || parsedVendorId.toString();
-
- // Determine revision type
- const revisionType = nextRevisionNumber === 1 ? "INITIAL" : "RESUBMISSION";
-
- // Create the submission
- const [newSubmission] = await db
- .insert(stageSubmissions)
- .values({
- stageId,
- documentId: parsedDocumentId,
- revisionNumber: nextRevisionNumber,
- revisionCode,
- revisionType,
- submissionStatus: "SUBMITTED",
- submittedBy,
- submittedByEmail: submittedByEmail || undefined,
- submittedAt: new Date(),
- submissionTitle: submissionTitle || undefined,
- submissionDescription: submissionDescription || undefined,
- vendorId: parsedVendorId,
- vendorCode,
- totalFiles: attachment ? 1 : 0,
- totalFileSize: attachment ? attachment.size : 0,
- syncStatus: "pending",
- syncVersion: 0,
- lastModifiedBy: "EVCP",
- totalFilesToSync: attachment ? 1 : 0,
- syncedFilesCount: 0,
- failedFilesCount: 0,
- })
- .returning();
-
- if (!newSubmission) {
- return {
- success: false,
- error: "Failed to create submission",
- };
- }
-
- // Upload attachment if provided
- if (attachment) {
- try {
- // Generate unique filename
- const fileExtension = customFileName.split(".").pop() || "docx";
- const timestamp = Date.now();
- const randomString = crypto.randomBytes(8).toString("hex");
- const uniqueFileName = `submissions/${parsedDocumentId}/${stageId}/${timestamp}_${randomString}.${fileExtension}`;
-
- // Calculate checksum
- const buffer = await attachment.arrayBuffer();
- const checksum = crypto
- .createHash("md5")
- .update(Buffer.from(buffer))
- .digest("hex");
-
- // Upload to Vercel Blob (or your storage solution)
- const blob = await put(uniqueFileName, attachment, {
- access: "public",
- contentType: attachment.type || "application/octet-stream",
- });
-
- // Create attachment record
- await db.insert(stageSubmissionAttachments).values({
- submissionId: newSubmission.id,
- fileName: uniqueFileName,
- originalFileName: customFileName,
- fileType: attachment.type || "application/octet-stream",
- fileExtension,
- fileSize: attachment.size,
- storageType: "S3",
- storagePath: blob.url,
- storageUrl: blob.url,
- mimeType: attachment.type || "application/octet-stream",
- checksum,
- documentType: "DOCUMENT",
- uploadedBy: submittedBy,
- uploadedAt: new Date(),
- status: "ACTIVE",
- syncStatus: "pending",
- syncVersion: 0,
- lastModifiedBy: "EVCP",
- isPublic: false,
- });
-
- // Update submission with file info
- await db
- .update(stageSubmissions)
- .set({
- totalFiles: 1,
- totalFileSize: attachment.size,
- totalFilesToSync: 1,
- updatedAt: new Date(),
- })
- .where(eq(stageSubmissions.id, newSubmission.id));
- } catch (uploadError) {
- console.error("Error uploading attachment:", uploadError);
-
- // Rollback: Delete the submission if file upload fails
- await db
- .delete(stageSubmissions)
- .where(eq(stageSubmissions.id, newSubmission.id));
-
- return {
- success: false,
- error: uploadError instanceof Error
- ? `File upload failed: ${uploadError.message}`
- : "File upload failed",
- };
- }
- }
-
- // Update stage status to SUBMITTED
- await db
- .update(stageIssueStages)
- .set({
- stageStatus: "SUBMITTED",
- updatedAt: new Date(),
- })
- .where(eq(stageIssueStages.id, stageId));
-
- // Update document's last modified info
- await db
- .update(stageDocuments)
- .set({
- lastModifiedBy: "EVCP",
- syncVersion: document.syncVersion + 1,
- updatedAt: new Date(),
- })
- .where(eq(stageDocuments.id, parsedDocumentId));
-
- // Revalidate relevant paths
- revalidatePath(`/projects/${document.projectId}/documents`);
- revalidatePath(`/vendor/documents`);
- revalidatePath(`/vendor/submissions`);
-
- return {
- success: true,
- submissionId: newSubmission.id,
- };
- } catch (error) {
- console.error("Error creating submission:", error);
- return {
- success: false,
- error: error instanceof Error ? error.message : "Unknown error occurred",
- };
- }
-}
-
-// Additional helper: Update submission status
-export async function updateSubmissionStatus(
- submissionId: number,
- status: string,
- reviewedBy?: string,
- reviewComments?: string
-): Promise<CreateSubmissionResult> {
- try {
- const reviewStatus =
- status === "APPROVED" ? "APPROVED" :
- status === "REJECTED" ? "REJECTED" :
- "PENDING";
-
- await db
- .update(stageSubmissions)
- .set({
- submissionStatus: status,
- reviewStatus,
- reviewComments: reviewComments || undefined,
- reviewedBy: reviewedBy || undefined,
- reviewedAt: new Date(),
- updatedAt: new Date(),
- })
- .where(eq(stageSubmissions.id, submissionId));
-
- // If approved, update stage status
- if (status === "APPROVED") {
- const [submission] = await db
- .select({ stageId: stageSubmissions.stageId })
- .from(stageSubmissions)
- .where(eq(stageSubmissions.id, submissionId))
- .limit(1);
-
- if (submission) {
- await db
- .update(stageIssueStages)
- .set({
- stageStatus: "APPROVED",
- actualDate: new Date().toISOString().split('T')[0],
- updatedAt: new Date(),
- })
- .where(eq(stageIssueStages.id, submission.stageId));
- }
- }
-
- return { success: true };
- } catch (error) {
- console.error("Error updating submission status:", error);
- return {
- success: false,
- error: error instanceof Error ? error.message : "Failed to update submission status"
- };
- }
-}
-
-// Helper: Delete submission
-export async function deleteSubmissionAction(
- submissionId: number
-): Promise<CreateSubmissionResult> {
- try {
- // Get submission info first
- const [submission] = await db
- .select()
- .from(stageSubmissions)
- .where(eq(stageSubmissions.id, submissionId))
- .limit(1);
-
- if (!submission) {
- return {
- success: false,
- error: "Submission not found",
- };
- }
-
- // Delete attachments from storage
- const attachments = await db
- .select()
- .from(stageSubmissionAttachments)
- .where(eq(stageSubmissionAttachments.submissionId, submissionId));
-
- // TODO: Delete files from blob storage
- // for (const attachment of attachments) {
- // await del(attachment.storageUrl);
- // }
-
- // Delete submission (cascade will delete attachments)
- await db
- .delete(stageSubmissions)
- .where(eq(stageSubmissions.id, submissionId));
-
- // Revalidate paths
- revalidatePath(`/vendor/documents`);
- revalidatePath(`/vendor/submissions`);
-
- return { success: true };
- } catch (error) {
- console.error("Error deleting submission:", error);
- return {
- success: false,
- error: error instanceof Error ? error.message : "Failed to delete submission",
- };
- }
} \ No newline at end of file
diff --git a/types/table.d.ts b/types/table.d.ts
index 9fc96687..d4053cf1 100644
--- a/types/table.d.ts
+++ b/types/table.d.ts
@@ -54,7 +54,7 @@ export type Filter<TData> = Prettify<
export interface DataTableRowAction<TData> {
row: Row<TData>
- type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" | "resend"
+ type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission"
}
export interface QueryBuilderOpts {