diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-26 10:13:54 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-26 10:13:54 +0000 |
| commit | b807f0e8007d297ab282a4329bfbdeb3afcb63a7 (patch) | |
| tree | 46610f3260c1b90ff92462f4828e891b1edc3218 /lib/tags-plant | |
| parent | c775a993930e806f56ea116941574015ee518170 (diff) | |
(대표님) EDP 작업사항
Diffstat (limited to 'lib/tags-plant')
| -rw-r--r-- | lib/tags-plant/column-builder.service.ts | 34 | ||||
| -rw-r--r-- | lib/tags-plant/queries.ts | 68 | ||||
| -rw-r--r-- | lib/tags-plant/repository.ts | 42 | ||||
| -rw-r--r-- | lib/tags-plant/service.ts | 729 | ||||
| -rw-r--r-- | lib/tags-plant/table/add-tag-dialog.tsx | 18 | ||||
| -rw-r--r-- | lib/tags-plant/table/delete-tags-dialog.tsx | 12 | ||||
| -rw-r--r-- | lib/tags-plant/table/tag-table.tsx | 775 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-export.tsx | 5 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-table-floating-bar.tsx | 5 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-table-toolbar-actions.tsx | 42 | ||||
| -rw-r--r-- | lib/tags-plant/table/update-tag-sheet.tsx | 13 |
11 files changed, 1381 insertions, 362 deletions
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) |
