summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-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
18 files changed, 3981 insertions, 632 deletions
diff --git a/lib/forms-plant/services.ts b/lib/forms-plant/services.ts
index 219f36e4..7e1976e6 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,
- formMetas,
+ formEntries,formEntriesPlant,
+ formMetas,formsPlant,
forms,
tagClassAttributes,
tagClasses,
- tags,
+ tags,tagsPlant,
tagSubfieldOptions,
tagSubfields,
tagTypeClassFormMappings,
tagTypes,
- vendorDataReportTemps,
- VendorDataReportTemps,
+ vendorDataReportTempsPlant,
+ VendorDataReportTempsPlant,
} 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,6 +29,7 @@ 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>;
@@ -164,7 +165,8 @@ export interface EditableFieldsInfo {
// TAG별 편집 가능 필드 조회 함수
export async function getEditableFieldsByTag(
- contractItemId: number,
+ projectCode: string,
+ packageCode: string,
projectId: number
): Promise<Map<string, string[]>> {
try {
@@ -174,8 +176,11 @@ export async function getEditableFieldsByTag(
tagNo: tags.tagNo,
tagClass: tags.class
})
- .from(tags)
- .where(eq(tags.contractItemId, contractItemId));
+ .from(tagsPlant)
+ .where(
+ eq(tagsPlant.projectCode, projectCode),
+ eq(tagsPlant.packageCode, packageCode),
+ );
const editableFieldsMap = new Map<string, string[]>();
@@ -228,26 +233,17 @@ export async function getEditableFieldsByTag(
* data가 배열이면 그 배열을 반환,
* 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱.
*/
-export async function getFormData(formCode: string, contractItemId: number) {
+export async function getFormData(formCode: string, projectCode: string, packageCode:string) {
try {
- // 기존 로직으로 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 project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- const projectId = contractItemResult[0].projectId;
+ const projectId = project.id;
const metaRows = await db
.select()
@@ -269,14 +265,15 @@ export async function getFormData(formCode: string, contractItemId: number) {
const entryRows = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
)
)
- .orderBy(desc(formEntries.updatedAt))
+ .orderBy(desc(formEntriesPlant.updatedAt))
.limit(1);
const entry = entryRows[0] ?? null;
@@ -321,7 +318,7 @@ export async function getFormData(formCode: string, contractItemId: number) {
}
// *** 새로 추가: 편집 가능 필드 정보 계산 ***
- const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId);
+ const editableFieldsMap = await getEditableFieldsByTag(projectCode,packageCode ,projectId);
return { columns, data, editableFieldsMap };
@@ -331,24 +328,16 @@ export async function getFormData(formCode: string, contractItemId: number) {
// Fallback logic (기존과 동일하게 editableFieldsMap 추가)
try {
- console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`);
-
- 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);
+ console.log(`[getFormData] Fallback DB query for (${formCode}, ${packageCode})`);
- if (contractItemResult.length === 0) {
- console.warn(`[getFormData] Fallback: No contract item found with ID: ${contractItemId}`);
- return { columns: null, data: [], editableFieldsMap: new Map() };
- }
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- const projectId = contractItemResult[0].projectId;
+ const projectId = project.id;
const metaRows = await db
.select()
@@ -370,14 +359,15 @@ export async function getFormData(formCode: string, contractItemId: number) {
const entryRows = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
)
- .orderBy(desc(formEntries.updatedAt))
+ .orderBy(desc(formEntriesPlant.updatedAt))
.limit(1);
const entry = entryRows[0] ?? null;
@@ -406,7 +396,7 @@ export async function getFormData(formCode: string, contractItemId: number) {
}
// Fallback에서도 편집 가능 필드 정보 계산
- const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId);
+ const editableFieldsMap = await getEditableFieldsByTag(projectCode, packageCode, projectId);
return { columns, data, projectId, editableFieldsMap };
} catch (dbError) {
@@ -415,7 +405,7 @@ export async function getFormData(formCode: string, contractItemId: number) {
}
}
}
-/**1
+/**
* contractId와 formCode(itemCode)를 사용하여 contractItemId를 찾는 서버 액션
*
* @param contractId - 계약 ID
@@ -517,24 +507,26 @@ export async function getPackageCodeById(contractItemId: number): Promise<string
export async function syncMissingTags(
- contractItemId: number,
+ projectCode: string,
+ packageCode: string,
formCode: string
) {
// (1) Ensure there's a row in `forms` matching (contractItemId, formCode).
const [formRow] = await db
.select()
- .from(forms)
+ .from(formsPlant)
.where(
and(
- eq(forms.contractItemId, contractItemId),
- eq(forms.formCode, formCode)
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formCode)
)
)
.limit(1);
if (!formRow) {
throw new Error(
- `Form not found for contractItemId=${contractItemId}, formCode=${formCode}`
+ `Form not found for projectCode=${projectCode}, formCode=${formCode}`
);
}
@@ -558,26 +550,28 @@ export async function syncMissingTags(
// (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs.
const tagRows = await db
.select()
- .from(tags)
- .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions)));
+ .from(tagsPlant)
+ .where(and(eq(tagsPlant.packageCode, packageCode),eq(tagsPlant.projectCode, projectCode), or(...orConditions)));
// (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode).
let [entry] = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.contractItemId, contractItemId),
- eq(formEntries.formCode, formCode)
+ eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.formCode, formCode)
)
)
.limit(1);
if (!entry) {
const [inserted] = await db
- .insert(formEntries)
+ .insert(formEntriesPlant)
.values({
- contractItemId,
+ projectCode,
+ packageCode,
formCode,
data: [], // Initialize with empty array
})
@@ -646,13 +640,13 @@ export async function syncMissingTags(
// (6) 실제로 추가되거나 수정되거나 삭제된 게 있다면 DB에 반영
if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) {
await db
- .update(formEntries)
+ .update(formEntriesPlant)
.set({ data: updatedData })
- .where(eq(formEntries.id, entry.id));
+ .where(eq(formEntriesPlant.id, entry.id));
}
// 캐시 무효화 등 후처리
- revalidateTag(`form-data-${formCode}-${contractItemId}`);
+ // revalidateTag(`form-data-${formCode}-${projectCode}`);
return { createdCount, updatedCount, deletedCount };
}
@@ -681,7 +675,8 @@ export interface UpdateResponse {
export async function updateFormDataInDB(
formCode: string,
- contractItemId: number,
+ projectCode: string,
+ packageCode: string,
newData: Record<string, any>
): Promise<UpdateResponse> {
try {
@@ -697,11 +692,12 @@ export async function updateFormDataInDB(
// 2) row 찾기 (단 하나)
const entries = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntries.projectCode, projectCode),
+ eq(formEntries.packageCode, packageCode),
)
)
.limit(1);
@@ -756,12 +752,12 @@ export async function updateFormDataInDB(
// 6) DB UPDATE
try {
await db
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedArray,
updatedAt: new Date(), // 업데이트 시간도 갱신
})
- .where(eq(formEntries.id, entry.id));
+ .where(eq(formEntriesPlant.id, entry.id));
} catch (dbError) {
console.error("Database update error:", dbError);
@@ -781,7 +777,7 @@ export async function updateFormDataInDB(
// 7) Cache 무효화
try {
// 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정
- const cacheTag = `form-data-${formCode}-${contractItemId}`;
+ const cacheTag = `form-data-${formCode}-${projectCode}`;
console.log(cacheTag, "update")
revalidateTag(cacheTag);
} catch (cacheError) {
@@ -814,7 +810,8 @@ export async function updateFormDataInDB(
export async function updateFormDataBatchInDB(
formCode: string,
- contractItemId: number,
+ projectCode: string,
+ packageCode: string,
newDataArray: Record<string, any>[]
): Promise<UpdateResponse> {
try {
@@ -839,11 +836,12 @@ export async function updateFormDataBatchInDB(
// 1) DB에서 현재 데이터 가져오기
const entries = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
)
)
.limit(1);
@@ -851,7 +849,7 @@ export async function updateFormDataBatchInDB(
if (!entries || entries.length === 0) {
return {
success: false,
- message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`,
+ message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, projectCode=${projectCode})`,
};
}
@@ -918,12 +916,12 @@ export async function updateFormDataBatchInDB(
// 3) DB에 한 번만 저장
try {
await db
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedArray,
updatedAt: new Date(),
})
- .where(eq(formEntries.id, entry.id));
+ .where(eq(formEntriesPlant.id, entry.id));
} catch (dbError) {
console.error("Database update error:", dbError);
@@ -952,7 +950,7 @@ export async function updateFormDataBatchInDB(
// 4) 캐시 무효화
try {
- const cacheTag = `form-data-${formCode}-${contractItemId}`;
+ const cacheTag = `form-data-${formCode}-${projectCode}`;
console.log(`Cache invalidated: ${cacheTag}`);
revalidateTag(cacheTag);
} catch (cacheError) {
@@ -1043,26 +1041,37 @@ export async function fetchFormMetadata(
}
type GetReportFileList = (
- packageId: string,
- formCode: string
+ projectCode: string,
+ packageCode: string,
+ formCode: string,
+ mode: string
) => Promise<{
formId: number;
}>;
-export const getFormId: GetReportFileList = async (packageId, formCode) => {
+export const getFormId: GetReportFileList = async (projectCode, packageCode, formCode, mode) => {
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(forms)
- .where(
- and(
- eq(forms.formCode, formCode),
- eq(forms.contractItemId, Number(packageId))
- )
- );
+ .from(formsPlant)
+ .where(and(...conditions));
if (!targetForm) {
throw new Error("Not Found Target Form");
@@ -1072,30 +1081,34 @@ export const getFormId: GetReportFileList = async (packageId, formCode) => {
result.formId = formId;
} catch (err) {
+ console.error("Error getting form ID:", err);
} finally {
return result;
}
};
type getReportTempList = (
- packageId: number,
+ projectCode: string,
+ packageCode: string,
formId: number
-) => Promise<VendorDataReportTemps[]>;
+) => Promise<VendorDataReportTempsPlant[]>;
export const getReportTempList: getReportTempList = async (
- packageId,
+ projectCode,
+ packageCode,
formId
) => {
- let result: VendorDataReportTemps[] = [];
+ let result: VendorDataReportTempsPlant[] = [];
try {
result = await db
.select()
- .from(vendorDataReportTemps)
+ .from(vendorDataReportTempsPlant)
.where(
and(
- eq(vendorDataReportTemps.contractItemId, packageId),
- eq(vendorDataReportTemps.formId, formId)
+ eq(vendorDataReportTempsPlant.projectCode, projectCode),
+ eq(vendorDataReportTempsPlant.packageCode, packageCode),
+ eq(vendorDataReportTempsPlant.formId, formId)
)
);
} catch (err) {
@@ -1105,7 +1118,8 @@ export const getReportTempList: getReportTempList = async (
};
export async function uploadReportTemp(
- packageId: number,
+ projectCode: string,
+ packageCode: string,
formId: number,
formData: FormData
) {
@@ -1128,9 +1142,10 @@ export async function uploadReportTemp(
return db.transaction(async (tx) => {
// 파일 정보를 테이블에 저장
await tx
- .insert(vendorDataReportTemps)
+ .insert(vendorDataReportTempsPlant)
.values({
- contractItemId: packageId,
+ projectCode,
+ packageCode,
formId: formId,
fileName: customFileName,
filePath: saveResult.publicPath!,
@@ -1160,16 +1175,16 @@ export const deleteReportTempFile: deleteReportTempFile = async (id) => {
return db.transaction(async (tx) => {
const [targetTempFile] = await tx
.select()
- .from(vendorDataReportTemps)
- .where(eq(vendorDataReportTemps.id, id));
+ .from(vendorDataReportTempsPlant)
+ .where(eq(vendorDataReportTempsPlant.id, id));
if (!targetTempFile) {
throw new Error("해당 Template File을 찾을 수 없습니다.");
}
await tx
- .delete(vendorDataReportTemps)
- .where(eq(vendorDataReportTemps.id, id));
+ .delete(vendorDataReportTempsPlant)
+ .where(eq(vendorDataReportTempsPlant.id, id));
const { filePath } = targetTempFile;
@@ -1310,7 +1325,8 @@ async function transformDataToSEDPFormat(
formCode: string,
objectCode: string,
projectNo: string,
- contractItemId: number, // Add contractItemId parameter
+ packageCode: string,
+ contractItemId: string,
designerNo: string = "253213"
): Promise<SEDPDataItem[]> {
// Create a map for quick column lookup
@@ -1331,9 +1347,6 @@ 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>();
@@ -1341,97 +1354,15 @@ async function transformDataToSEDPFormat(
const transformedItems = [];
for (const row of tableData) {
+ let tagClassCode = "";
- 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
-
+ // Get tagClass code if TAG_NO exists
if (row.TAG_NO && contractItemId) {
- // Check cache first
const cacheKey = `${contractItemId}-${row.TAG_NO}`;
- 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)) {
+ if (tagClassCodeCache.has(cacheKey)) {
tagClassCode = tagClassCodeCache.get(cacheKey)!;
- } else if (!tagClassCode) {
+ } else {
try {
const tagResult = await db.query.tags.findFirst({
where: and(
@@ -1440,22 +1371,20 @@ async function transformDataToSEDPFormat(
)
});
- if (tagResult && tagResult.tagClassId) {
+ if (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 code for tag ${row.TAG_NO}:`, error);
- // Cache empty string as fallback
+ console.error(`Error fetching tagClass for tag ${row.TAG_NO}:`, error);
tagClassCodeCache.set(cacheKey, "");
}
}
@@ -1466,17 +1395,16 @@ async function transformDataToSEDPFormat(
TAG_NO: row.TAG_NO || "",
TAG_DESC: row.TAG_DESC || "",
ATTRIBUTES: [],
- // SCOPE: objectCode,
SCOPE: packageCode,
- TOOLID: "eVCP", // Changed from VDCS
+ TOOLID: "eVCP",
ITM_NO: row.TAG_NO || "",
- OP_DELETE: row.status === "Deleted", // Set OP_DELETE based on status
+ OP_DELETE: row.status === "Deleted",
MAIN_YN: true,
LAST_REV_YN: true,
CRTER_NO: designerNo,
CHGER_NO: designerNo,
- TYPE: formCode, // Use packageCode instead of formCode
- CLS_ID: tagClassCode, // Add CLS_ID with tagClass code
+ TYPE: formCode,
+ CLS_ID: tagClassCode,
PROJ_NO: projectNo,
REV_NO: "00",
CRTE_DTM: currentTimestamp,
@@ -1522,7 +1450,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 (type assertion to ensure it's a number)
+ // Store in cache for future use
uomFactorCache.set(column.uomId, factor);
}
} else {
@@ -1533,7 +1461,7 @@ async function transformDataToSEDPFormat(
}
}
- // Apply the factor if we got one
+ // Apply the factor if needed (currently commented out)
// if (factor !== undefined && typeof value === 'number') {
// value = value * factor;
// }
@@ -1541,7 +1469,7 @@ async function transformDataToSEDPFormat(
const attribute: SEDPAttribute = {
NAME: key,
- VALUE: String(value), // 모든 값을 문자열로 변환
+ VALUE: String(value),
UOM: column?.uom || "",
CLS_ID: tagClassCode || "",
};
@@ -1569,7 +1497,7 @@ export async function transformFormDataToSEDP(
formCode: string,
objectCode: string,
projectNo: string,
- contractItemId: number, // Add contractItemId parameter
+ packageCode: string, // Add contractItemId parameter
designerNo: string = "253213"
): Promise<SEDPDataItem[]> {
return transformDataToSEDPFormat(
@@ -1578,7 +1506,7 @@ export async function transformFormDataToSEDP(
formCode,
objectCode,
projectNo,
- contractItemId, // Pass contractItemId
+ packageCode,
designerNo
);
}
@@ -1599,6 +1527,20 @@ 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})
@@ -1679,13 +1621,13 @@ export async function sendDataToSEDP(
export async function sendFormDataToSEDP(
formCode: string,
projectId: number,
- contractItemId: number, // contractItemId 파라미터 추가
+ projectCode: string, // contractItemId 파라미터 추가
+ packageCode: string, // 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({
@@ -1728,7 +1670,7 @@ export async function sendFormDataToSEDP(
formCode,
objectCode,
projectCode,
- contractItemId // Add contractItemId parameter
+ packageCode // Add contractItemId parameter
);
// 4. Send to SEDP API
@@ -1739,11 +1681,12 @@ export async function sendFormDataToSEDP(
// Get the current formEntries data
const entries = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
)
)
.limit(1);
@@ -1778,17 +1721,17 @@ export async function sendFormDataToSEDP(
// Update the database
await db
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedDataArray,
updatedAt: new Date()
})
- .where(eq(formEntries.id, entry.id));
+ .where(eq(formEntriesPlant.id, entry.id));
console.log(`Updated status for ${sentTagNumbers.size} tags to "Sent to S-EDP"`);
}
} else {
- console.warn(`No formEntries found for formCode: ${formCode}, contractItemId: ${contractItemId}`);
+ console.warn(`No formEntriesPlant found for formCode: ${formCode}`);
}
} catch (statusUpdateError) {
// Status 업데이트 실패는 경고로만 처리 (SEDP 전송은 성공했으므로)
@@ -1812,12 +1755,14 @@ export async function sendFormDataToSEDP(
export async function deleteFormDataByTags({
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
tagIdxs,
projectId,
}: {
formCode: string
- contractItemId: number
+ projectCode: string
+ packageCode: string
tagIdxs: string[]
projectId?: number
}): Promise<{
@@ -1830,25 +1775,26 @@ export async function deleteFormDataByTags({
}> {
try {
// 입력 검증
- if (!formCode || !contractItemId || !Array.isArray(tagIdxs) || tagIdxs.length === 0) {
+ if (!formCode || !projectCode || !Array.isArray(tagIdxs) || tagIdxs.length === 0) {
return {
error: "Missing required parameters: formCode, contractItemId, tagIdxs",
}
}
- console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagIdxs:`, tagIdxs)
+ console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, projectCode: ${projectCode}, tagIdxs:`, tagIdxs)
// 1. 트랜잭션 전에 삭제할 항목들을 미리 조회하여 저장 (S-EDP 전송용)
const entryForSedp = await db
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
)
- .orderBy(desc(formEntries.updatedAt))
+ .orderBy(desc(formEntriesPlant.updatedAt))
.limit(1)
let itemsToSendToSedp: Record<string, unknown>[] = []
@@ -1868,14 +1814,15 @@ export async function deleteFormDataByTags({
// 2-1. 현재 formEntry 데이터 가져오기
const currentEntryResult = await tx
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
)
- .orderBy(desc(formEntries.updatedAt))
+ .orderBy(desc(formEntriesPlant.updatedAt))
.limit(1)
if (currentEntryResult.length === 0) {
@@ -1903,14 +1850,15 @@ export async function deleteFormDataByTags({
// 2-3. tags 테이블에서 해당 태그들 삭제
const deletedTagsResult = await tx
- .delete(tags)
+ .delete(tagsPlant)
.where(
and(
- eq(tags.contractItemId, contractItemId),
- inArray(tags.tagIdx, tagIdxs)
+ eq(tagsPlant.projectCode, projectCode),
+ eq(tagsPlant.packageCode, packageCode),
+ inArray(tagsPlant.tagIdx, tagIdxs)
)
)
- .returning({ tagNo: tags.tagNo })
+ .returning({ tagNo: tagsPlant.tagNo })
const deletedTagsCount = deletedTagsResult.length
@@ -1919,15 +1867,16 @@ export async function deleteFormDataByTags({
// 2-4. formEntries 데이터 업데이트 (삭제된 항목 제외)
await tx
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedData,
updatedAt: new Date(),
})
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
)
)
@@ -2020,7 +1969,8 @@ export async function deleteFormDataByTags({
const sedpResult = await sendFormDataToSEDP(
formCode,
projectId,
- contractItemId,
+ projectCode,
+ packageCode,
uniqueDeletedItems as GenericData[],
formMetaResult.columns as DataTableColumnJSON[]
)
@@ -2079,11 +2029,13 @@ export async function deleteFormDataByTags({
*/
export async function excludeFormDataByTags({
formCode,
- contractItemId,
+ projectCode,
+ packageCode,
tagNumbers,
}: {
formCode: string
- contractItemId: number
+ projectCode: string
+ packageCode: string
tagNumbers: string[]
}): Promise<{
error?: string
@@ -2092,27 +2044,28 @@ export async function excludeFormDataByTags({
}> {
try {
// 입력 검증
- if (!formCode || !contractItemId || !Array.isArray(tagNumbers) || tagNumbers.length === 0) {
+ if (!formCode || !projectCode || !Array.isArray(tagNumbers) || tagNumbers.length === 0) {
return {
- error: "Missing required parameters: formCode, contractItemId, tagNumbers",
+ error: "Missing required parameters: formCode, projectCode, tagNumbers",
}
}
- console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNumbers:`, tagNumbers)
+ console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, projectCode: ${projectCode}, tagNumbers:`, tagNumbers)
// 트랜잭션으로 안전하게 처리
const result = await db.transaction(async (tx) => {
// 1. 현재 formEntry 데이터 가져오기
const currentEntryResult = await tx
.select()
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
)
- .orderBy(desc(formEntries.updatedAt))
+ .orderBy(desc(formEntriesPlant.updatedAt))
.limit(1)
if (currentEntryResult.length === 0) {
@@ -2146,15 +2099,16 @@ export async function excludeFormDataByTags({
// 3. formEntries 데이터 업데이트
await tx
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedData,
updatedAt: new Date(),
})
.where(
and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, contractItemId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
)
@@ -2165,7 +2119,7 @@ export async function excludeFormDataByTags({
})
// 4. 캐시 무효화
- const cacheKey = `form-data-${formCode}-${contractItemId}`
+ const cacheKey = `form-data-${formCode}-${packageCode}`
revalidateTag(cacheKey)
console.log(`[EXCLUDE ACTION] Transaction completed successfully`)
diff --git a/lib/forms-plant/stat.ts b/lib/forms-plant/stat.ts
index f13bab61..f734e782 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, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema"
+import { vendors, contracts, contractItems, forms,formsPlant,formEntriesPlant, formEntries, formMetas, tags,tagsPlant, 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, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> {
+export async function getFormStatusByVendor(projectId: number, projectCode: string, packageCode: string, formCode: string): Promise<FormStatusByVendor[]> {
try {
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
@@ -244,15 +244,16 @@ export async function getFormStatusByVendor(projectId: number, contractItemId: n
// 4. contractItem별 forms 조회
const formsList = await db
.select({
- id: forms.id,
- formCode: forms.formCode,
- contractItemId: forms.contractItemId
+ id: formsPlant.id,
+ formCode: formsPlant.formCode,
+ contractItemId: formsPlant.contractItemId
})
- .from(forms)
+ .from(formsPlant)
.where(
and(
- eq(forms.contractItemId, contractItemId),
- eq(forms.formCode, formCode)
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formCode)
)
)
@@ -261,20 +262,21 @@ export async function getFormStatusByVendor(projectId: number, contractItemId: n
// 5. formEntries 조회
const entriesList = await db
.select({
- id: formEntries.id,
- formCode: formEntries.formCode,
- data: formEntries.data
+ id: formEntriesPlant.id,
+ formCode: formEntriesPlant.formCode,
+ data: formEntriesPlant.data
})
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.contractItemId, contractItemId),
- eq(formEntries.formCode, formCode)
+ eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.formCode, formCode)
)
)
// 6. TAG별 편집 가능 필드 조회
- const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId)
+ const editableFieldsByTag = await getEditableFieldsByTag(projectCode,packageCode, projectId)
const vendorStatusList: VendorFormStatus[] = []
diff --git a/lib/sedp/get-form-tags-plant.ts b/lib/sedp/get-form-tags-plant.ts
new file mode 100644
index 00000000..176f1b3f
--- /dev/null
+++ b/lib/sedp/get-form-tags-plant.ts
@@ -0,0 +1,933 @@
+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
new file mode 100644
index 00000000..d1957db4
--- /dev/null
+++ b/lib/sedp/get-tags-plant.ts
@@ -0,0 +1,639 @@
+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 904d27ba..a6d473ad 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: any[];
+ MAP_ATT: MapAttribute2[];
MAP_CLS_ID: string[];
MAP_OPER: any | null;
LNK_ATT: LinkAttribute[];
@@ -157,6 +157,13 @@ 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
new file mode 100644
index 00000000..9a552d6e
--- /dev/null
+++ b/lib/tags-plant/column-builder.service.ts
@@ -0,0 +1,34 @@
+// 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
new file mode 100644
index 00000000..a0d28b1e
--- /dev/null
+++ b/lib/tags-plant/queries.ts
@@ -0,0 +1,68 @@
+// 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 b5d48335..bbe36f66 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 } from "@/db/schema/vendorData";
+import { NewTag, tags, tagsPlant } from "@/db/schema/vendorData";
import {
eq,
inArray,
@@ -69,3 +69,43 @@ 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 778ab89d..02bd33be 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, tagClasses, tags, tagSubfieldOptions, tagSubfields, tagTypes } from "@/db/schema/vendorData"
+import { formEntries, forms,items,formsPlant, tagClasses, tags, tagsPlant, tagSubfieldOptions, tagSubfields, tagTypes,formEntriesPlant } from "@/db/schema"
// 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 } from "./repository";
+import { countTags, insertTag, selectTags, selectTagsPlant, countTagsPlant,insertTagPlant } from "./repository";
import { getErrorMessage } from "../handle-error";
import { getFormMappingsByTagType } from './form-mapping-service';
import { contractItems, contracts } from "@/db/schema/contract";
@@ -32,7 +32,8 @@ function generateTagIdx(): string {
return randomBytes(12).toString('hex'); // 12바이트 = 24자리 16진수
}
-export async function getTags(input: GetTagsSchema, packagesId: number) {
+
+export async function getTagsPlant(input: GetTagsSchema, projectCode: string,packageCode: string ) {
// return unstable_cache(
// async () => {
@@ -41,7 +42,7 @@ export async function getTags(input: GetTagsSchema, packagesId: number) {
// (1) advancedWhere
const advancedWhere = filterColumns({
- table: tags,
+ table: tagsPlant,
filters: input.filters,
joinOperator: input.joinOperator,
});
@@ -51,31 +52,31 @@ export async function getTags(input: GetTagsSchema, packagesId: number) {
if (input.search) {
const s = `%${input.search}%`;
globalWhere = or(
- ilike(tags.tagNo, s),
- ilike(tags.tagType, s),
- ilike(tags.description, s)
+ ilike(tagsPlant.tagNo, s),
+ ilike(tagsPlant.tagType, s),
+ ilike(tagsPlant.description, s)
);
}
- // (4) 최종 where
- const finalWhere = and(advancedWhere, globalWhere, eq(tags.contractItemId, packagesId));
+ // (4) 최종 projectCode
+ const finalWhere = and(advancedWhere, globalWhere, eq(tagsPlant.projectCode, projectCode), eq(tagsPlant.packageCode, packageCode));
// (5) 정렬
const orderBy =
input.sort.length > 0
? input.sort.map((item) =>
- item.desc ? desc(tags[item.id]) : asc(tags[item.id])
+ item.desc ? desc(tagsPlant[item.id]) : asc(tagsPlant[item.id])
)
- : [asc(tags.createdAt)];
+ : [asc(tagsPlant.createdAt)];
// 트랜잭션 내부에서 Repository 호출
const { data, total } = await db.transaction(async (tx) => {
- const data = await selectTags(tx, {
+ const data = await selectTagsPlant(tx, {
where: finalWhere,
orderBy,
offset,
limit: input.perPage,
});
- const total = await countTags(tx, finalWhere);
+ const total = await countTagsPlant(tx, finalWhere);
return { data, total };
@@ -101,9 +102,10 @@ export async function getTags(input: GetTagsSchema, packagesId: number) {
export async function createTag(
formData: CreateTagSchema,
- selectedPackageId: number | null
+ projectCode: string,
+ packageCode: string,
) {
- if (!selectedPackageId) {
+ if (!projectCode) {
return { error: "No selectedPackageId provided" }
}
@@ -119,33 +121,23 @@ export async function createTag(
try {
// 하나의 트랜잭션에서 모든 작업 수행
return await db.transaction(async (tx) => {
- // 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)
-
- if (contractItemResult.length === 0) {
- return { error: "Contract item not found" }
- }
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- const contractId = contractItemResult[0].contractId
- const projectId = contractItemResult[0].projectId
+ const projectId = project.id
// 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인
const duplicateCheck = await tx
.select({ count: sql<number>`count(*)` })
- .from(tags)
- .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
+ .from(tagsPlant)
.where(
and(
- eq(contractItems.contractId, contractId),
- eq(tags.tagNo, validated.data.tagNo)
+ eq(tagsPlant.projectCode, projectCode),
+ eq(tagsPlant.tagNo, validated.data.tagNo)
)
)
@@ -182,16 +174,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: forms.id, im: forms.im, eng: forms.eng }) // eng 필드도 추가로 조회
- .from(forms)
+ .select({ id: formsPlant.id, im: formsPlant.im, eng: formsPlant.eng }) // eng 필드도 추가로 조회
+ .from(formsPlant)
.where(
and(
- eq(forms.contractItemId, selectedPackageId),
- eq(forms.formCode, formMapping.formCode)
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formMapping.formCode)
)
)
.limit(1)
@@ -219,9 +211,9 @@ export async function createTag(
if (shouldUpdate) {
await tx
- .update(forms)
+ .update(formsPlant)
.set(updateValues)
- .where(eq(forms.id, formId))
+ .where(eq(formsPlant.id, formId))
console.log(`Form ${formId} updated with:`, updateValues)
}
@@ -235,7 +227,8 @@ export async function createTag(
} else {
// 존재하지 않으면 새로 생성
const insertValues: any = {
- contractItemId: selectedPackageId,
+ projectCode: projectCode,
+ packageCode: packageCode,
formCode: formMapping.formCode,
formName: formMapping.formName,
im: true,
@@ -247,9 +240,9 @@ export async function createTag(
}
const insertResult = await tx
- .insert(forms)
+ .insert(formsPlant)
.values(insertValues)
- .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName })
+ .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName })
console.log("insertResult:", insertResult)
formId = insertResult[0].id
@@ -273,8 +266,9 @@ export async function createTag(
console.log(`[CREATE TAG] Generated tagIdx: ${generatedTagIdx}`);
// 5) 새 Tag 생성 (tagIdx 추가)
- const [newTag] = await insertTag(tx, {
- contractItemId: selectedPackageId,
+ const [newTag] = await insertTagPlant(tx, {
+ packageCode:packageCode,
+ projectCode:projectCode,
formId: primaryFormId,
tagIdx: generatedTagIdx, // 🆕 생성된 16진수 24자리 추가
tagNo: validated.data.tagNo,
@@ -283,7 +277,6 @@ 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) {
@@ -291,8 +284,9 @@ export async function createTag(
// 기존 formEntry 가져오기
const existingEntry = await tx.query.formEntries.findFirst({
where: and(
- eq(formEntries.formCode, form.formCode),
- eq(formEntries.contractItemId, selectedPackageId)
+ eq(formEntriesPlant.formCode, form.formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
)
});
@@ -329,12 +323,12 @@ export async function createTag(
const updatedData = [...existingData, newTagData];
await tx
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedData,
updatedAt: new Date()
})
- .where(eq(formEntries.id, existingEntry.id));
+ .where(eq(formEntriesPlant.id, existingEntry.id));
console.log(`[CREATE TAG] Added tag ${validated.data.tagNo} with tagIdx ${generatedTagIdx} to existing formEntry for form ${form.formCode}`);
} else {
@@ -342,9 +336,10 @@ export async function createTag(
}
} else {
// formEntry가 없는 경우 새로 생성 (TAG_IDX 포함)
- await tx.insert(formEntries).values({
+ await tx.insert(formEntriesPlant).values({
formCode: form.formCode,
- contractItemId: selectedPackageId,
+ projectCode: projectCode,
+ packageCode: packageCode,
data: [newTagData],
createdAt: new Date(),
updatedAt: new Date(),
@@ -358,16 +353,6 @@ 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,
@@ -666,10 +651,11 @@ export async function createTagInForm(
export async function updateTag(
formData: UpdateTagSchema & { id: number },
- selectedPackageId: number | null
+ projectCode: string,
+ packageCode: string,
) {
- if (!selectedPackageId) {
- return { error: "No selectedPackageId provided" }
+ if (!projectCode) {
+ return { error: "No projectCode provided" }
}
if (!formData.id) {
@@ -701,35 +687,25 @@ export async function updateTag(
const originalTag = existingTag[0]
- // 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 project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- const contractId = contractItemResult[0].contractId
- const projectId = contractItemResult[0].projectId
+ const projectId = project.id
// 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인
if (originalTag.tagNo !== validated.data.tagNo) {
const duplicateCheck = await tx
.select({ count: sql<number>`count(*)` })
- .from(tags)
- .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
+ .from(tagsPlant)
.where(
and(
- eq(contractItems.contractId, contractId),
- eq(tags.tagNo, validated.data.tagNo),
- ne(tags.id, formData.id) // 자기 자신은 제외
+ eq(tagsPlant.projectCode, projectCode),
+ eq(tagsPlant.tagNo, validated.data.tagNo),
+ ne(tagsPlant.id, formData.id) // 자기 자신은 제외
)
)
@@ -774,11 +750,12 @@ export async function updateTag(
// 이미 존재하는 폼인지 확인
const existingForm = await tx
.select({ id: forms.id })
- .from(forms)
+ .from(formsPlant)
.where(
and(
- eq(forms.contractItemId, selectedPackageId),
- eq(forms.formCode, formMapping.formCode)
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formMapping.formCode)
)
)
.limit(1)
@@ -796,13 +773,14 @@ export async function updateTag(
} else {
// 존재하지 않으면 새로 생성
const insertResult = await tx
- .insert(forms)
+ .insert(formsPlant)
.values({
- contractItemId: selectedPackageId,
+ projectCode,
+ packageCode,
formCode: formMapping.formCode,
formName: formMapping.formName,
})
- .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName })
+ .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName })
formId = insertResult[0].id
createdOrExistingForms.push({
@@ -823,9 +801,10 @@ export async function updateTag(
// 5) 태그 업데이트
const [updatedTag] = await tx
- .update(tags)
+ .update(tagsPlant)
.set({
- contractItemId: selectedPackageId,
+ projectCode,
+ packageCode,
formId: primaryFormId,
tagNo: validated.data.tagNo,
class: validated.data.class,
@@ -833,12 +812,9 @@ export async function updateTag(
description: validated.data.description ?? null,
updatedAt: new Date(),
})
- .where(eq(tags.id, formData.id))
+ .where(eq(tagsPlant.id, formData.id))
.returning()
- // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시)
- revalidateTag(`tags-${selectedPackageId}`)
- revalidateTag(`forms-${selectedPackageId}`)
revalidateTag("tags")
// 7) 성공 시 반환
@@ -867,7 +843,8 @@ export interface TagInputData {
// 새로운 서버 액션
export async function bulkCreateTags(
tagsfromExcel: TagInputData[],
- selectedPackageId: number
+ projectCode: string,
+ packageCode: string
) {
unstable_noStore();
@@ -879,31 +856,22 @@ export async function bulkCreateTags(
// 단일 트랜잭션으로 모든 작업 처리
return await db.transaction(async (tx) => {
// 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만)
- 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 project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- const contractId = contractItemResult[0].contractId;
- const projectId = contractItemResult[0].projectId; // projectId 추출
+ const projectId = project.id
// 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(contractItems.contractId, contractId),
+ eq(tags.projectCode, projectCode),
inArray(tags.tagNo, tagNos)
));
@@ -969,12 +937,13 @@ export async function bulkCreateTags(
for (const formMapping of formMappings) {
// 해당 폼이 이미 존재하는지 확인
const existingForm = await tx
- .select({ id: forms.id, im: forms.im })
- .from(forms)
+ .select({ id: formsPlant.id, im: formsPlant.im })
+ .from(formsPlant)
.where(
and(
- eq(forms.contractItemId, selectedPackageId),
- eq(forms.formCode, formMapping.formCode)
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.formCode, formMapping.formCode)
)
)
.limit(1);
@@ -987,9 +956,9 @@ export async function bulkCreateTags(
// im 필드 업데이트 (필요한 경우)
if (existingForm[0].im !== true) {
await tx
- .update(forms)
+ .update(formsPlant)
.set({ im: true })
- .where(eq(forms.id, formId));
+ .where(eq(formsPlant.id, formId));
}
createdOrExistingForms.push({
@@ -1001,14 +970,15 @@ export async function bulkCreateTags(
} else {
// 존재하지 않으면 새로 생성
const insertResult = await tx
- .insert(forms)
+ .insert(formsPlant)
.values({
- contractItemId: selectedPackageId,
+ packageCode:packageCode,
+ projectCode:projectCode,
formCode: formMapping.formCode,
formName: formMapping.formName,
im: true
})
- .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName });
+ .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName });
formId = insertResult[0].id;
createdOrExistingForms.push({
@@ -1048,8 +1018,9 @@ export async function bulkCreateTags(
}
// 태그 생성
- const [newTag] = await insertTag(tx, {
- contractItemId: selectedPackageId,
+ const [newTag] = await insertTagPlant(tx, {
+ packageCode:packageCode,
+ projectCode:projectCode,
formId: primaryFormId,
tagNo: tagData.tagNo,
class: tagData.class || "",
@@ -1067,14 +1038,15 @@ export async function bulkCreateTags(
});
}
- // 4. formEntries 업데이트 처리
+ // 4. formEntriesPlant 업데이트 처리
for (const [formCode, newTagsData] of tagsByFormCode.entries()) {
try {
// 기존 formEntry 가져오기
- const existingEntry = await tx.query.formEntries.findFirst({
+ const existingEntry = await tx.query.formEntriesPlant.findFirst({
where: and(
- eq(formEntries.formCode, formCode),
- eq(formEntries.contractItemId, selectedPackageId)
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntriesPlant.projectCode, projectCode)
)
});
@@ -1103,12 +1075,12 @@ export async function bulkCreateTags(
const updatedData = [...existingData, ...newUniqueTagsData];
await tx
- .update(formEntries)
+ .update(formEntriesPlant)
.set({
data: updatedData,
updatedAt: new Date()
})
- .where(eq(formEntries.id, existingEntry.id));
+ .where(eq(formEntriesPlant.id, existingEntry.id));
console.log(`[BULK CREATE] Added ${newUniqueTagsData.length} tags to existing formEntry for form ${formCode}`);
} else {
@@ -1116,9 +1088,10 @@ export async function bulkCreateTags(
}
} else {
// formEntry가 없는 경우 새로 생성
- await tx.insert(formEntries).values({
+ await tx.insert(formEntriesPlant).values({
formCode: formCode,
- contractItemId: selectedPackageId,
+ projectCode:projectCode,
+ packageCode:packageCode,
data: newTagsData,
createdAt: new Date(),
updatedAt: new Date(),
@@ -1132,16 +1105,6 @@ 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: {
@@ -1160,7 +1123,8 @@ export async function bulkCreateTags(
/** 복수 삭제 */
interface RemoveTagsInput {
ids: number[];
- selectedPackageId: number;
+ projectCode: string;
+ packageCode: string;
}
@@ -1178,36 +1142,29 @@ function removeTagFromDataJson(
export async function removeTags(input: RemoveTagsInput) {
unstable_noStore() // React 서버 액션 무상태 함수
- const { ids, selectedPackageId } = input
+ const { ids, projectCode, packageCode } = input
try {
await db.transaction(async (tx) => {
- 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;
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
+ const projectId = project.id;
// 1) 삭제 대상 tag들을 미리 조회
const tagsToDelete = await tx
.select({
- id: tags.id,
- tagNo: tags.tagNo,
- tagType: tags.tagType,
- class: tags.class,
+ id: tagsPlant.id,
+ tagNo: tagsPlant.tagNo,
+ tagType: tagsPlant.tagType,
+ class: tagsPlant.class,
})
- .from(tags)
- .where(inArray(tags.id, ids))
+ .from(tagsPlant)
+ .where(inArray(tagsPlant.id, ids))
// 2) 태그 타입과 클래스의 고유 조합 추출
const uniqueTypeClassCombinations = [...new Set(
@@ -1222,13 +1179,14 @@ export async function removeTags(input: RemoveTagsInput) {
// 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인
const otherTagsWithSameTypeClass = await tx
.select({ count: count() })
- .from(tags)
+ .from(tagsPlant)
.where(
and(
- eq(tags.tagType, tagType),
- classValue ? eq(tags.class, classValue) : isNull(tags.class),
- not(inArray(tags.id, ids)), // 현재 삭제 중인 태그들은 제외
- eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인
+ 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 내에서만 확인
)
)
@@ -1249,21 +1207,23 @@ export async function removeTags(input: RemoveTagsInput) {
if (otherTagsWithSameTypeClass[0].count === 0) {
// 폼 삭제
await tx
- .delete(forms)
+ .delete(formsPlant)
.where(
and(
- eq(forms.contractItemId, selectedPackageId),
- eq(forms.formCode, formMapping.formCode)
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formMapping.formCode)
)
)
// formEntries 테이블에서도 해당 formCode 관련 데이터 삭제
await tx
- .delete(formEntries)
+ .delete(formEntriesPlant)
.where(
and(
- eq(formEntries.contractItemId, selectedPackageId),
- eq(formEntries.formCode, formMapping.formCode)
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntriesPlant.formCode, formMapping.formCode)
)
)
}
@@ -1271,14 +1231,15 @@ export async function removeTags(input: RemoveTagsInput) {
else if (relevantTagNos.length > 0) {
const formEntryRecords = await tx
.select({
- id: formEntries.id,
- data: formEntries.data,
+ id: formEntriesPlant.id,
+ data: formEntriesPlant.data,
})
- .from(formEntries)
+ .from(formEntriesPlant)
.where(
and(
- eq(formEntries.contractItemId, selectedPackageId),
- eq(formEntries.formCode, formMapping.formCode)
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode),
+ eq(formEntriesPlant.formCode, formMapping.formCode)
)
)
@@ -1305,9 +1266,6 @@ 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) {
@@ -1328,25 +1286,26 @@ export interface ClassOption {
* Class 옵션 목록을 가져오는 함수
* 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함
*/
-export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> {
+export async function getClassOptions(
+ packageCode: string,
+ projectCode: string
+): Promise<UpdatedClassOption[]> {
try {
- // 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))
+ // 1. 프로젝트 정보 조회
+ const projectInfo = await db
+ .select()
+ .from(projects)
+ .where(eq(projects.code, projectCode))
.limit(1);
- if (packageInfo.length === 0) {
- throw new Error(`Contract item with ID ${selectedPackageId} not found`);
+ if (projectInfo.length === 0) {
+ throw new Error(`Project with code ${projectCode} not found`);
}
- const projectId = packageInfo[0].projectId;
+ const projectId = projectInfo[0].id;
+
- // 2. 태그 클래스들을 서브클래스 정보와 함께 조회
+ // 3. 태그 클래스들을 서브클래스 정보와 함께 조회
const tagClassesWithSubclasses = await db
.select({
id: tagClasses.id,
@@ -1360,8 +1319,8 @@ export async function getClassOptions(selectedPackageId: number): Promise<Update
.where(eq(tagClasses.projectId, projectId))
.orderBy(tagClasses.code);
- // 3. 태그 타입 정보도 함께 조회 (description을 위해)
- const tagTypesMap = new Map();
+ // 4. 태그 타입 정보도 함께 조회 (description을 위해)
+ const tagTypesMap = new Map<string, string>();
const tagTypesList = await db
.select({
code: tagTypes.code,
@@ -1370,21 +1329,24 @@ export async function getClassOptions(selectedPackageId: number): Promise<Update
.from(tagTypes)
.where(eq(tagTypes.projectId, projectId));
- tagTypesList.forEach(tagType => {
+ tagTypesList.forEach((tagType) => {
tagTypesMap.set(tagType.code, tagType.description);
});
- // 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 || {},
- }));
+ // 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 || {},
+ })
+ );
return classOptions;
} catch (error) {
@@ -1392,6 +1354,8 @@ export async function getClassOptions(selectedPackageId: number): Promise<Update
throw new Error("Failed to fetch class options");
}
}
+
+
interface SubFieldDef {
name: string
label: string
@@ -1403,26 +1367,20 @@ interface SubFieldDef {
export async function getSubfieldsByTagType(
tagTypeCode: string,
- selectedPackageId: number,
+ projectCode: string,
subclassRemark: string = "",
subclass: string = "",
) {
try {
- // 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);
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
- if (packageInfo.length === 0) {
- throw new Error(`Contract item with ID ${selectedPackageId} not found`);
- }
- const projectId = packageInfo[0].projectId;
+ const projectId = project.id
// 2. 올바른 projectId를 사용하여 tagSubfields 조회
const rows = await db
@@ -1623,29 +1581,314 @@ export interface TagTypeOption {
label: string; // tagTypes.description 값
}
-export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> {
+export async function getProjectIdFromContractItemId(
+ projectCode: string
+): Promise<number | null> {
try {
// First get the contractId from contractItems
- const contractItem = await db.query.contractItems.findFirst({
- where: eq(contractItems.id, contractItemId),
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
columns: {
- contractId: true
+ id: true
}
});
- if (!contractItem) return null;
-
- // Then get the projectId from contracts
- const contract = await db.query.contracts.findFirst({
- where: eq(contracts.id, contractItem.contractId),
- columns: {
- projectId: true
- }
- });
+ if (!project) return null;
- return contract?.projectId || 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';
+
+
+/**
+ * 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
+ }
+ }
+
+ // 2-4. DB에 저장
+ if (formsToInsert.length > 0) {
+ await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing()
+ console.log(`${formsToInsert.length}개의 IM 폼을 DB에 저장했습니다.`)
+ }
+
+ return formInfos
+ } catch (error) {
+ console.error("IM 폼 가져오기 실패:", error)
+ throw new Error("Failed to fetch IM forms")
+ }
} \ 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 9c82bf1a..41731f63 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/service"
+} from "@/lib/tags-plant/service"
import { ScrollArea } from "@/components/ui/scroll-area"
// Updated to support multiple rows and subclass
@@ -98,10 +98,11 @@ interface UpdatedClassOption extends ClassOption {
}
interface AddTagDialogProps {
- selectedPackageId: number
+ projectCode: string
+ packageCode: string
}
-export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
+export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) {
const router = useRouter()
const params = useParams()
const lng = (params?.lng as string) || "ko"
@@ -125,7 +126,6 @@ export function AddTagDialog({ selectedPackageId }: 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({ selectedPackageId }: AddTagDialogProps) {
setIsLoadingClasses(true)
try {
// getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정
- const result = await getClassOptions(selectedPackageId)
+ const result = await getClassOptions(packageCode, projectCode)
setClassOptions(result)
} catch (err) {
toast.error(t("toast.classOptionsLoadFailed"))
@@ -147,7 +147,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
if (open) {
loadClassOptions()
}
- }, [open, selectedPackageId])
+ }, [open, projectCode, packageCode])
// ---------------
// react-hook-form with fieldArray support for multiple rows
@@ -176,7 +176,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
setIsLoadingSubFields(true)
try {
// 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가)
- const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark, subclass)
+ const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass)
const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({
name: field.name,
label: field.label,
@@ -313,7 +313,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
// Submit handler for multiple tags (서브클래스 정보 포함)
// ---------------
async function onSubmit(data: MultiTagFormValues) {
- if (!selectedPackageId) {
+ if (!projectCode) {
toast.error(t("toast.noSelectedPackageId"));
return;
}
@@ -343,7 +343,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
};
try {
- const res = await createTag(tagData, selectedPackageId);
+ const res = await createTag(tagData, projectCode, packageCode);
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 6a024cda..69a4f4a6 100644
--- a/lib/tags-plant/table/delete-tags-dialog.tsx
+++ b/lib/tags-plant/table/delete-tags-dialog.tsx
@@ -4,7 +4,6 @@ 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 {
@@ -27,15 +26,15 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
-
-import { removeTags } from "@/lib//tags/service"
+import { removeTags } from "@/lib//tags-plant/service"
import { Tag } from "@/db/schema/vendorData"
interface DeleteTasksDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
tags: Row<Tag>["original"][]
showTrigger?: boolean
- selectedPackageId: number
+ projectCode: string
+ packageCode: string
onSuccess?: () => void
}
@@ -43,7 +42,8 @@ export function DeleteTagsDialog({
tags,
showTrigger = true,
onSuccess,
- selectedPackageId,
+ projectCode,
+ packageCode,
...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),selectedPackageId
+ ids: tags.map((tag) => tag.id),projectCode, packageCode
})
if (error) {
diff --git a/lib/tags-plant/table/tag-table.tsx b/lib/tags-plant/table/tag-table.tsx
index 1986d933..2fdcd5fc 100644
--- a/lib/tags-plant/table/tag-table.tsx
+++ b/lib/tags-plant/table/tag-table.tsx
@@ -1,3 +1,4 @@
+// components/vendor-data-plant/tags-table.tsx
"use client"
import * as React from "react"
@@ -6,40 +7,177 @@ 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 { 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 { ClientDataTable } from "@/components/client-data-table/data-table"
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 {
- promises: Promise< [ Awaited<ReturnType<typeof getTags>> ] >
- selectedPackageId: number
+ 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;
}
-export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
- // 1) 데이터를 가져옴 (server component -> use(...) pattern)
- const [{ data, pageCount }] = React.use(promises)
+export function TagsTable({
+ projectCode,
+ packageCode,
+}: TagsTableProps) {
+ const router = useRouter()
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)
- const columns = React.useMemo(
- () => getColumns({ setRowAction }),
- [setRowAction]
- )
+ // 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])
// Filter fields
const filterFields: DataTableFilterField<Tag>[] = [
@@ -67,6 +205,11 @@ export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
type: "text",
},
{
+ id: "class",
+ label: "Class",
+ type: "text",
+ },
+ {
id: "createdAt",
label: "Created at",
type: "date",
@@ -78,78 +221,562 @@ export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
},
]
- // 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",
+ // 선택된 행 개수
+ 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])
- const [isCompact, setIsCompact] = React.useState<boolean>(false)
+ // Import 파일 선택
+ const handleImportClick = () => {
+ fileInputRef.current?.click()
+ }
+ // Import 파일 처리
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ const file = e.target.files?.[0]
+ if (!file) return
- const handleCompactChange = React.useCallback((compact: boolean) => {
- setIsCompact(compact)
+ 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)
+
+ 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)
+ }
+
+ // 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)
+ }
+ }
}, [])
-
-
+
+ // 로딩 중
+ 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 (
<>
- <DataTable
- table={table}
- compact={isCompact}
-
- floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>}
+ <ClientDataTable
+ data={tableData}
+ columns={columns}
+ advancedFilterFields={advancedFilterFields}
+ autoSizeColumns
+ onSelectedRowsChange={setSelectedRowsData}
+ clearSelection={clearSelection}
+ onTableReady={(table) => {
+ tableRef.current = table
+ }}
>
- <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>
+ <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}/>
+ {/* 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}
+ />
+
+ {/* Update Sheet */}
<UpdateTagSheet
open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
+ onOpenChange={(open) => {
+ if (!open) setRowAction(null)
+ }}
tag={rowAction?.row.original ?? null}
- selectedPackageId={selectedPackageId}
+ 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
+ )
+ )
+ }
+ }}
/>
-
+ {/* Delete Dialog */}
<DeleteTagsDialog
- open={rowAction?.type === "delete"}
- onOpenChange={() => setRowAction(null)}
- tags={rowAction?.row.original ? [rowAction?.row.original] : []}
+ tags={deleteTarget}
+ packageCode={packageCode}
+ projectCode={projectCode}
+ open={deleteDialogOpen}
+ onOpenChange={(open) => {
+ if (!open) {
+ setDeleteDialogOpen(false)
+ setDeleteTarget([])
+ }
+ }}
+ onSuccess={handleDeleteSuccess}
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 fa85148d..a3255a0b 100644
--- a/lib/tags-plant/table/tags-export.tsx
+++ b/lib/tags-plant/table/tags-export.tsx
@@ -15,7 +15,8 @@ import { getClassOptions } from "../service"
*/
export async function exportTagsToExcel(
table: Table<Tag>,
- selectedPackageId: number,
+ packageCode: string,
+ projectCode: string,
{
filename = "Tags",
excludeColumns = ["select", "actions", "createdAt", "updatedAt"],
@@ -42,7 +43,7 @@ export async function exportTagsToExcel(
const worksheet = workbook.addWorksheet("Tags")
// 3. Tag Class 옵션 가져오기
- const classOptions = await getClassOptions(selectedPackageId)
+ const classOptions = await getClassOptions(packageCode, projectCode)
// 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 8d55b7ac..eadbfb12 100644
--- a/lib/tags-plant/table/tags-table-floating-bar.tsx
+++ b/lib/tags-plant/table/tags-table-floating-bar.tsx
@@ -36,12 +36,13 @@ import { Tag } from "@/db/schema/vendorData"
interface TagsTableFloatingBarProps {
table: Table<Tag>
- selectedPackageId: number
+ packageCode: string
+ projectCode: string
}
-export function TagsTableFloatingBar({ table, selectedPackageId }: TagsTableFloatingBarProps) {
+export function TagsTableFloatingBar({ table, packageCode, projectCode}: 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 cc2d82b4..c80a600e 100644
--- a/lib/tags-plant/table/tags-table-toolbar-actions.tsx
+++ b/lib/tags-plant/table/tags-table-toolbar-actions.tsx
@@ -52,7 +52,8 @@ interface TagsTableToolbarActionsProps {
/** react-table 객체 */
table: Table<Tag>
/** 현재 선택된 패키지 ID */
- selectedPackageId: number
+ packageCode: string
+ projectCode: string
/** 현재 태그 목록(상태) */
tableData: Tag[]
/** 태그 목록을 갱신하는 setState */
@@ -68,7 +69,8 @@ interface TagsTableToolbarActionsProps {
*/
export function TagsTableToolbarActions({
table,
- selectedPackageId,
+ packageCode,
+ projectCode,
tableData,
selectedMode
}: TagsTableToolbarActionsProps) {
@@ -94,7 +96,7 @@ export function TagsTableToolbarActions({
React.useEffect(() => {
const loadClassOptions = async () => {
try {
- const options = await getClassOptions(selectedPackageId)
+ const options = await getClassOptions(packageCode, projectCode)
setClassOptions(options)
} catch (error) {
console.error("Failed to load class options:", error)
@@ -102,7 +104,7 @@ export function TagsTableToolbarActions({
}
loadClassOptions()
- }, [selectedPackageId])
+ }, [packageCode, projectCode])
// 숨겨진 <input>을 클릭
function handleImportClick() {
@@ -135,12 +137,11 @@ 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 (selectedPackageId) {
+ if (packageCode && projectCode) {
try {
- const pid = await getProjectIdFromContractItemId(selectedPackageId);
+ const pid = await getProjectIdFromContractItemId(projectCode );
setProjectId(pid);
} catch (error) {
console.error("Failed to fetch project ID:", error);
@@ -150,7 +151,7 @@ export function TagsTableToolbarActions({
};
fetchProjectId();
- }, [selectedPackageId]);
+ }, [projectCode]);
// 특정 attributesId에 대한 옵션 가져오기
const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => {
@@ -195,7 +196,7 @@ export function TagsTableToolbarActions({
}
try {
- const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId)
+ const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode)
// API 응답을 SubFieldDef 형식으로 변환
const formattedSubFields: SubFieldDef[] = subFields.map(field => ({
@@ -478,7 +479,7 @@ export function TagsTableToolbarActions({
if (tagNo) {
// 이미 tableData 내 존재 여부
const dup = tableData.find(
- (t) => t.contractItemId === selectedPackageId && t.tagNo === tagNo
+ (t) => t.tagNo === tagNo
)
if (dup) {
errorMsg += `TagNo '${tagNo}' already exists. `
@@ -523,7 +524,8 @@ export function TagsTableToolbarActions({
// 정상 행을 importedRows에 추가
importedRows.push({
id: 0, // 임시
- contractItemId: selectedPackageId,
+ packageCode: packageCode,
+ projectCode: projectCode,
formId: null,
tagNo,
tagType: finalTagType, // ← 코드로 저장할지, Description으로 저장할지 결정
@@ -552,7 +554,7 @@ export function TagsTableToolbarActions({
// 정상 행이 있으면 태그 생성 요청
if (importedRows.length > 0) {
- const result = await bulkCreateTags(importedRows, selectedPackageId);
+ const result = await bulkCreateTags(importedRows, projectCode, packageCode);
if ("error" in result) {
toast.error(result.error);
} else {
@@ -575,8 +577,8 @@ export function TagsTableToolbarActions({
setIsExporting(true)
// 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출
- await exportTagsToExcel(table, selectedPackageId, {
- filename: `Tags_${selectedPackageId}`,
+ await exportTagsToExcel(table, packageCode,projectCode, {
+ filename: `Tags_${packageCode}_${projectCode}`,
excludeColumns: ["select", "actions", "createdAt", "updatedAt"],
})
@@ -594,10 +596,11 @@ export function TagsTableToolbarActions({
setIsLoading(true)
// API 엔드포인트 호출 - 작업 시작만 요청
- const response = await fetch('/api/cron/tags/start', {
+ const response = await fetch('/api/cron/tags-plant/start', {
method: 'POST',
body: JSON.stringify({
- packageId: selectedPackageId,
+ projectCode: projectCode,
+ packageCode: packageCode,
mode: selectedMode // 모드 정보 추가
})
})
@@ -638,7 +641,7 @@ export function TagsTableToolbarActions({
// 5초마다 상태 확인
pollingRef.current = setInterval(async () => {
try {
- const response = await fetch(`/api/cron/tags/status?id=${id}`)
+ const response = await fetch(`/api/cron/tags-plant/status?id=${id}`)
if (!response.ok) {
throw new Error('Failed to get tag import status')
@@ -699,7 +702,8 @@ export function TagsTableToolbarActions({
.getFilteredSelectedRowModel()
.rows.map((row) => row.original)}
onSuccess={() => table.toggleAllRowsSelected(false)}
- selectedPackageId={selectedPackageId}
+ projectCode={projectCode}
+ packageCode={packageCode}
/>
) : null}
<Button
@@ -715,7 +719,7 @@ export function TagsTableToolbarActions({
</span>
</Button>
- <AddTagDialog selectedPackageId={selectedPackageId} />
+ <AddTagDialog projectCode={projectCode} packageCode={packageCode} />
{/* Import */}
<Button
diff --git a/lib/tags-plant/table/update-tag-sheet.tsx b/lib/tags-plant/table/update-tag-sheet.tsx
index 613abaa9..2be1e732 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/service"
+import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags-plant/service"
// SubFieldDef 인터페이스
interface SubFieldDef {
@@ -84,10 +84,11 @@ type UpdateTagSchema = z.infer<typeof updateTagSchema> & Record<string, string>
interface UpdateTagSheetProps extends React.ComponentPropsWithRef<typeof Sheet> {
tag: Tag | null
- selectedPackageId: number
+ packageCode: string
+ projectCode: string
}
-export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSheetProps) {
+export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: UpdateTagSheetProps) {
const [isUpdatePending, startUpdateTransition] = React.useTransition()
const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([])
const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null)
@@ -110,7 +111,7 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh
setIsLoadingClasses(true)
try {
- const result = await getClassOptions(selectedPackageId)
+ const result = await getClassOptions(packageCode, projectCode)
setClassOptions(result)
} catch (err) {
toast.error("클래스 옵션을 불러오는데 실패했습니다.")
@@ -164,7 +165,7 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh
async function loadSubFieldsByTagTypeCode(tagTypeCode: string) {
setIsLoadingSubFields(true)
try {
- const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId)
+ const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode)
const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({
name: field.name,
label: field.label,
@@ -221,7 +222,7 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh
),
}
- const result = await updateTag(tagData, selectedPackageId)
+ const result = await updateTag(tagData, projectCode,packageCode )
if ("error" in result) {
toast.error(result.error)
diff --git a/lib/vendor-data/services.ts b/lib/vendor-data/services.ts
index 8c8b21d2..fe4e56ae 100644
--- a/lib/vendor-data/services.ts
+++ b/lib/vendor-data/services.ts
@@ -62,6 +62,8 @@ 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))
@@ -126,3 +128,94 @@ 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 bf2b0b7a..48e3fa3f 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 { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu"
+import { stageSubmissions, stageDocuments, stageIssueStages,documentAttachments, documents, issueStages, revisions, stageDocumentsView,vendorDocumentsView ,stageSubmissionAttachments, StageIssueStage, StageDocumentsView, StageDocument,} 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 } from "@/db/schema"
+import { contractItems, projects, items,contracts } from "@/db/schema"
import { saveFile } from "../file-stroage"
import path from "path"
@@ -494,4 +494,706 @@ 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