diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
| commit | 5870b73785715d1585531e655c06d8c068eb64ac (patch) | |
| tree | 1d19e1482f5210cc56e778158b51e810f9717c46 /lib/tags-plant | |
| parent | 95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff) | |
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
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, 362 insertions, 1381 deletions
diff --git a/lib/tags-plant/column-builder.service.ts b/lib/tags-plant/column-builder.service.ts deleted file mode 100644 index 9a552d6e..00000000 --- a/lib/tags-plant/column-builder.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -// lib/vendor-data-plant/column-builder.service.ts -import { ColumnDef } from "@tanstack/react-table" -import { Tag } from "@/db/schema/vendorData" - -/** - * 동적 속성 컬럼 생성 (ATT_ID만 사용, 라벨 없음) - */ -export function createDynamicAttributeColumns( - attributeKeys: string[] -): ColumnDef<Tag>[] { - return attributeKeys.map(key => ({ - id: `attr_${key}`, - accessorFn: (row: Tag) => { - if (row.attributes && typeof row.attributes === 'object') { - return (row.attributes as Record<string, string>)[key] || '' - } - return '' - }, - header: key, // 단순 문자열로 반환 - cell: ({ getValue }) => { - const value = getValue() - return value as string || "-" - }, - meta: { - excelHeader: key - }, - enableSorting: true, - enableColumnFilter: true, - filterFn: "includesString", - enableResizing: true, - minSize: 100, - size: 150, - })) -}
\ No newline at end of file diff --git a/lib/tags-plant/queries.ts b/lib/tags-plant/queries.ts deleted file mode 100644 index a0d28b1e..00000000 --- a/lib/tags-plant/queries.ts +++ /dev/null @@ -1,68 +0,0 @@ -// lib/vendor-data-plant/queries.ts -"use server" - -import db from "@/db/db" - -import { tagsPlant } from "@/db/schema/vendorData" -import { eq, and } from "drizzle-orm" - -/** - * 모든 태그 가져오기 (클라이언트 렌더링용) - */ -export async function getAllTagsPlant( - projectCode: string, - packageCode: string -) { - try { - const tags = await db - .select() - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - .orderBy(tagsPlant.createdAt) - - return tags - } catch (error) { - console.error("Error fetching all tags:", error) - return [] - } -} - -/** - * 고유 속성 키 추출 - */ -export async function getUniqueAttributeKeys( - projectCode: string, - packageCode: string -): Promise<string[]> { - try { - const result = await db - .select({ - attributes: tagsPlant.attributes - }) - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - - const allKeys = new Set<string>() - - for (const row of result) { - if (row.attributes && typeof row.attributes === 'object') { - Object.keys(row.attributes).forEach(key => allKeys.add(key)) - } - } - - return Array.from(allKeys).sort() - } catch (error) { - console.error("Error getting unique attribute keys:", error) - return [] - } -}
\ No newline at end of file diff --git a/lib/tags-plant/repository.ts b/lib/tags-plant/repository.ts index bbe36f66..b5d48335 100644 --- a/lib/tags-plant/repository.ts +++ b/lib/tags-plant/repository.ts @@ -1,5 +1,5 @@ import db from "@/db/db"; -import { NewTag, tags, tagsPlant } from "@/db/schema/vendorData"; +import { NewTag, tags } from "@/db/schema/vendorData"; import { eq, inArray, @@ -69,43 +69,3 @@ export async function deleteTagsByIds( ) { return tx.delete(tags).where(inArray(tags.id, ids)); } - - -export async function selectTagsPlant( - tx: PgTransaction<any, any, any>, - params: { - where?: any; // drizzle-orm의 조건식 (and, eq...) 등 - orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[]; - offset?: number; - limit?: number; - } -) { - const { where, orderBy, offset = 0, limit = 10 } = params; - - return tx - .select() - .from(tagsPlant) - .where(where) - .orderBy(...(orderBy ?? [])) - .offset(offset) - .limit(limit); -} -/** 총 개수 count */ -export async function countTagsPlant( - tx: PgTransaction<any, any, any>, - where?: any -) { - const res = await tx.select({ count: count() }).from(tagsPlant).where(where); - return res[0]?.count ?? 0; -} - -export async function insertTagPlant( - tx: PgTransaction<any, any, any>, - data: NewTag // DB와 동일한 insert 가능한 타입 - ) { - // returning() 사용 시 배열로 돌아오므로 [0]만 리턴 - return tx - .insert(tagsPlant) - .values(data) - .returning({ id: tagsPlant.id, createdAt: tagsPlant.createdAt }); - }
\ No newline at end of file diff --git a/lib/tags-plant/service.ts b/lib/tags-plant/service.ts index 02bd33be..778ab89d 100644 --- a/lib/tags-plant/service.ts +++ b/lib/tags-plant/service.ts @@ -1,14 +1,14 @@ "use server" import db from "@/db/db" -import { formEntries, forms,items,formsPlant, tagClasses, tags, tagsPlant, tagSubfieldOptions, tagSubfields, tagTypes,formEntriesPlant } from "@/db/schema" +import { formEntries, forms, tagClasses, tags, tagSubfieldOptions, tagSubfields, tagTypes } from "@/db/schema/vendorData" // import { eq } from "drizzle-orm" import { createTagSchema, GetTagsSchema, updateTagSchema, UpdateTagSchema, type CreateTagSchema } from "./validations" import { revalidateTag, unstable_noStore } from "next/cache"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, sql, ne, count, isNull } from "drizzle-orm"; -import { countTags, insertTag, selectTags, selectTagsPlant, countTagsPlant,insertTagPlant } from "./repository"; +import { countTags, insertTag, selectTags } from "./repository"; import { getErrorMessage } from "../handle-error"; import { getFormMappingsByTagType } from './form-mapping-service'; import { contractItems, contracts } from "@/db/schema/contract"; @@ -32,8 +32,7 @@ function generateTagIdx(): string { return randomBytes(12).toString('hex'); // 12바이트 = 24자리 16진수 } - -export async function getTagsPlant(input: GetTagsSchema, projectCode: string,packageCode: string ) { +export async function getTags(input: GetTagsSchema, packagesId: number) { // return unstable_cache( // async () => { @@ -42,7 +41,7 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac // (1) advancedWhere const advancedWhere = filterColumns({ - table: tagsPlant, + table: tags, filters: input.filters, joinOperator: input.joinOperator, }); @@ -52,31 +51,31 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac if (input.search) { const s = `%${input.search}%`; globalWhere = or( - ilike(tagsPlant.tagNo, s), - ilike(tagsPlant.tagType, s), - ilike(tagsPlant.description, s) + ilike(tags.tagNo, s), + ilike(tags.tagType, s), + ilike(tags.description, s) ); } - // (4) 최종 projectCode - const finalWhere = and(advancedWhere, globalWhere, eq(tagsPlant.projectCode, projectCode), eq(tagsPlant.packageCode, packageCode)); + // (4) 최종 where + const finalWhere = and(advancedWhere, globalWhere, eq(tags.contractItemId, packagesId)); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => - item.desc ? desc(tagsPlant[item.id]) : asc(tagsPlant[item.id]) + item.desc ? desc(tags[item.id]) : asc(tags[item.id]) ) - : [asc(tagsPlant.createdAt)]; + : [asc(tags.createdAt)]; // 트랜잭션 내부에서 Repository 호출 const { data, total } = await db.transaction(async (tx) => { - const data = await selectTagsPlant(tx, { + const data = await selectTags(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); - const total = await countTagsPlant(tx, finalWhere); + const total = await countTags(tx, finalWhere); return { data, total }; @@ -102,10 +101,9 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac export async function createTag( formData: CreateTagSchema, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { + if (!selectedPackageId) { return { error: "No selectedPackageId provided" } } @@ -121,23 +119,33 @@ export async function createTag( try { // 하나의 트랜잭션에서 모든 작업 수행 return await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) - const projectId = project.id + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } + + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인 const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo) + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo) ) ) @@ -174,16 +182,16 @@ export async function createTag( const createdOrExistingForms: CreatedOrExistingForm[] = [] if (formMappings && formMappings.length > 0) { + console.log(selectedPackageId, formMappings) for (const formMapping of formMappings) { // 4-1) 이미 존재하는 폼인지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im, eng: formsPlant.eng }) // eng 필드도 추가로 조회 - .from(formsPlant) + .select({ id: forms.id, im: forms.im, eng: forms.eng }) // eng 필드도 추가로 조회 + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -211,9 +219,9 @@ export async function createTag( if (shouldUpdate) { await tx - .update(formsPlant) + .update(forms) .set(updateValues) - .where(eq(formsPlant.id, formId)) + .where(eq(forms.id, formId)) console.log(`Form ${formId} updated with:`, updateValues) } @@ -227,8 +235,7 @@ export async function createTag( } else { // 존재하지 않으면 새로 생성 const insertValues: any = { - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true, @@ -240,9 +247,9 @@ export async function createTag( } const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values(insertValues) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) console.log("insertResult:", insertResult) formId = insertResult[0].id @@ -266,9 +273,8 @@ export async function createTag( console.log(`[CREATE TAG] Generated tagIdx: ${generatedTagIdx}`); // 5) 새 Tag 생성 (tagIdx 추가) - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagIdx: generatedTagIdx, // 🆕 생성된 16진수 24자리 추가 tagNo: validated.data.tagNo, @@ -277,6 +283,7 @@ export async function createTag( description: validated.data.description ?? null, }) + console.log(`tags-${selectedPackageId}`, "create", newTag) // 6) 생성된 각 form에 대해 formEntries에 데이터 추가 (TAG_IDX 포함) for (const form of createdOrExistingForms) { @@ -284,9 +291,8 @@ export async function createTag( // 기존 formEntry 가져오기 const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, form.formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, form.formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -323,12 +329,12 @@ export async function createTag( const updatedData = [...existingData, newTagData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[CREATE TAG] Added tag ${validated.data.tagNo} with tagIdx ${generatedTagIdx} to existing formEntry for form ${form.formCode}`); } else { @@ -336,10 +342,9 @@ export async function createTag( } } else { // formEntry가 없는 경우 새로 생성 (TAG_IDX 포함) - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: form.formCode, - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, data: [newTagData], createdAt: new Date(), updatedAt: new Date(), @@ -353,6 +358,16 @@ export async function createTag( } } + // 7) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}-ENG`) + revalidateTag("tags") + + // 생성된 각 form의 캐시도 무효화 + createdOrExistingForms.forEach(form => { + revalidateTag(`form-data-${form.formCode}-${selectedPackageId}`) + }) + // 8) 성공 시 반환 (tagIdx 추가) return { success: true, @@ -651,11 +666,10 @@ export async function createTagInForm( export async function updateTag( formData: UpdateTagSchema & { id: number }, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { - return { error: "No projectCode provided" } + if (!selectedPackageId) { + return { error: "No selectedPackageId provided" } } if (!formData.id) { @@ -687,25 +701,35 @@ export async function updateTag( const originalTag = existingTag[0] - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 2) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } - const projectId = project.id + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인 if (originalTag.tagNo !== validated.data.tagNo) { const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo), - ne(tagsPlant.id, formData.id) // 자기 자신은 제외 + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo), + ne(tags.id, formData.id) // 자기 자신은 제외 ) ) @@ -750,12 +774,11 @@ export async function updateTag( // 이미 존재하는 폼인지 확인 const existingForm = await tx .select({ id: forms.id }) - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -773,14 +796,13 @@ export async function updateTag( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) formId = insertResult[0].id createdOrExistingForms.push({ @@ -801,10 +823,9 @@ export async function updateTag( // 5) 태그 업데이트 const [updatedTag] = await tx - .update(tagsPlant) + .update(tags) .set({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: validated.data.tagNo, class: validated.data.class, @@ -812,9 +833,12 @@ export async function updateTag( description: validated.data.description ?? null, updatedAt: new Date(), }) - .where(eq(tagsPlant.id, formData.id)) + .where(eq(tags.id, formData.id)) .returning() + // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) revalidateTag("tags") // 7) 성공 시 반환 @@ -843,8 +867,7 @@ export interface TagInputData { // 새로운 서버 액션 export async function bulkCreateTags( tagsfromExcel: TagInputData[], - projectCode: string, - packageCode: string + selectedPackageId: number ) { unstable_noStore(); @@ -856,22 +879,31 @@ export async function bulkCreateTags( // 단일 트랜잭션으로 모든 작업 처리 return await db.transaction(async (tx) => { // 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만) - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" }; + } - const projectId = project.id + const contractId = contractItemResult[0].contractId; + const projectId = contractItemResult[0].projectId; // projectId 추출 // 2. 모든 태그 번호 중복 검사 (한 번에) const tagNos = tagsfromExcel.map(tag => tag.tagNo); const duplicateCheck = await tx .select({ tagNo: tags.tagNo }) .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where(and( - eq(tags.projectCode, projectCode), + eq(contractItems.contractId, contractId), inArray(tags.tagNo, tagNos) )); @@ -937,13 +969,12 @@ export async function bulkCreateTags( for (const formMapping of formMappings) { // 해당 폼이 이미 존재하는지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im }) - .from(formsPlant) + .select({ id: forms.id, im: forms.im }) + .from(forms) .where( and( - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1); @@ -956,9 +987,9 @@ export async function bulkCreateTags( // im 필드 업데이트 (필요한 경우) if (existingForm[0].im !== true) { await tx - .update(formsPlant) + .update(forms) .set({ im: true }) - .where(eq(formsPlant.id, formId)); + .where(eq(forms.id, formId)); } createdOrExistingForms.push({ @@ -970,15 +1001,14 @@ export async function bulkCreateTags( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - packageCode:packageCode, - projectCode:projectCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }); + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }); formId = insertResult[0].id; createdOrExistingForms.push({ @@ -1018,9 +1048,8 @@ export async function bulkCreateTags( } // 태그 생성 - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: tagData.tagNo, class: tagData.class || "", @@ -1038,15 +1067,14 @@ export async function bulkCreateTags( }); } - // 4. formEntriesPlant 업데이트 처리 + // 4. formEntries 업데이트 처리 for (const [formCode, newTagsData] of tagsByFormCode.entries()) { try { // 기존 formEntry 가져오기 - const existingEntry = await tx.query.formEntriesPlant.findFirst({ + const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -1075,12 +1103,12 @@ export async function bulkCreateTags( const updatedData = [...existingData, ...newUniqueTagsData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[BULK CREATE] Added ${newUniqueTagsData.length} tags to existing formEntry for form ${formCode}`); } else { @@ -1088,10 +1116,9 @@ export async function bulkCreateTags( } } else { // formEntry가 없는 경우 새로 생성 - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: formCode, - projectCode:projectCode, - packageCode:packageCode, + contractItemId: selectedPackageId, data: newTagsData, createdAt: new Date(), updatedAt: new Date(), @@ -1105,6 +1132,16 @@ export async function bulkCreateTags( } } + // 5. 캐시 무효화 (한 번만) + revalidateTag(`tags-${selectedPackageId}`); + revalidateTag(`forms-${selectedPackageId}`); + revalidateTag("tags"); + + // 업데이트된 모든 form의 캐시도 무효화 + for (const formCode of tagsByFormCode.keys()) { + revalidateTag(`form-data-${formCode}-${selectedPackageId}`); + } + return { success: true, data: { @@ -1123,8 +1160,7 @@ export async function bulkCreateTags( /** 복수 삭제 */ interface RemoveTagsInput { ids: number[]; - projectCode: string; - packageCode: string; + selectedPackageId: number; } @@ -1142,29 +1178,36 @@ function removeTagFromDataJson( export async function removeTags(input: RemoveTagsInput) { unstable_noStore() // React 서버 액션 무상태 함수 - const { ids, projectCode, packageCode } = input + const { ids, selectedPackageId } = input try { await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); - const projectId = project.id; + const packageInfo = await tx + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } + + const projectId = packageInfo[0].projectId; // 1) 삭제 대상 tag들을 미리 조회 const tagsToDelete = await tx .select({ - id: tagsPlant.id, - tagNo: tagsPlant.tagNo, - tagType: tagsPlant.tagType, - class: tagsPlant.class, + id: tags.id, + tagNo: tags.tagNo, + tagType: tags.tagType, + class: tags.class, }) - .from(tagsPlant) - .where(inArray(tagsPlant.id, ids)) + .from(tags) + .where(inArray(tags.id, ids)) // 2) 태그 타입과 클래스의 고유 조합 추출 const uniqueTypeClassCombinations = [...new Set( @@ -1179,14 +1222,13 @@ export async function removeTags(input: RemoveTagsInput) { // 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인 const otherTagsWithSameTypeClass = await tx .select({ count: count() }) - .from(tagsPlant) + .from(tags) .where( and( - eq(tagsPlant.tagType, tagType), - classValue ? eq(tagsPlant.class, classValue) : isNull(tagsPlant.class), - not(inArray(tagsPlant.id, ids)), - eq(tags.packageCode, packageCode), - eq(tags.projectCode, projectCode) // 같은 contractItemId 내에서만 확인 + eq(tags.tagType, tagType), + classValue ? eq(tags.class, classValue) : isNull(tags.class), + not(inArray(tags.id, ids)), // 현재 삭제 중인 태그들은 제외 + eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인 ) ) @@ -1207,23 +1249,21 @@ export async function removeTags(input: RemoveTagsInput) { if (otherTagsWithSameTypeClass[0].count === 0) { // 폼 삭제 await tx - .delete(formsPlant) + .delete(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) // formEntries 테이블에서도 해당 formCode 관련 데이터 삭제 await tx - .delete(formEntriesPlant) + .delete(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) } @@ -1231,15 +1271,14 @@ export async function removeTags(input: RemoveTagsInput) { else if (relevantTagNos.length > 0) { const formEntryRecords = await tx .select({ - id: formEntriesPlant.id, - data: formEntriesPlant.data, + id: formEntries.id, + data: formEntries.data, }) - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) @@ -1266,6 +1305,9 @@ export async function removeTags(input: RemoveTagsInput) { await tx.delete(tags).where(inArray(tags.id, ids)) }) + // 5) 캐시 무효화 + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) return { data: null, error: null } } catch (err) { @@ -1286,26 +1328,25 @@ export interface ClassOption { * Class 옵션 목록을 가져오는 함수 * 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함 */ -export async function getClassOptions( - packageCode: string, - projectCode: string -): Promise<UpdatedClassOption[]> { +export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> { try { - // 1. 프로젝트 정보 조회 - const projectInfo = await db - .select() - .from(projects) - .where(eq(projects.code, projectCode)) + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) .limit(1); - if (projectInfo.length === 0) { - throw new Error(`Project with code ${projectCode} not found`); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); } - const projectId = projectInfo[0].id; - + const projectId = packageInfo[0].projectId; - // 3. 태그 클래스들을 서브클래스 정보와 함께 조회 + // 2. 태그 클래스들을 서브클래스 정보와 함께 조회 const tagClassesWithSubclasses = await db .select({ id: tagClasses.id, @@ -1319,8 +1360,8 @@ export async function getClassOptions( .where(eq(tagClasses.projectId, projectId)) .orderBy(tagClasses.code); - // 4. 태그 타입 정보도 함께 조회 (description을 위해) - const tagTypesMap = new Map<string, string>(); + // 3. 태그 타입 정보도 함께 조회 (description을 위해) + const tagTypesMap = new Map(); const tagTypesList = await db .select({ code: tagTypes.code, @@ -1329,24 +1370,21 @@ export async function getClassOptions( .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); - tagTypesList.forEach((tagType) => { + tagTypesList.forEach(tagType => { tagTypesMap.set(tagType.code, tagType.description); }); - // 5. 클래스 옵션으로 변환 - const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map( - (cls) => ({ - value: cls.code, - label: cls.label, - code: cls.code, - description: cls.label, - tagTypeCode: cls.tagTypeCode, - tagTypeDescription: - tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, - subclasses: cls.subclasses || [], - subclassRemark: cls.subclassRemark || {}, - }) - ); + // 4. 클래스 옵션으로 변환 + const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(cls => ({ + value: cls.code, + label: cls.label, + code: cls.code, + description: cls.label, + tagTypeCode: cls.tagTypeCode, + tagTypeDescription: tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, + subclasses: cls.subclasses || [], + subclassRemark: cls.subclassRemark || {}, + })); return classOptions; } catch (error) { @@ -1354,8 +1392,6 @@ export async function getClassOptions( throw new Error("Failed to fetch class options"); } } - - interface SubFieldDef { name: string label: string @@ -1367,20 +1403,26 @@ interface SubFieldDef { export async function getSubfieldsByTagType( tagTypeCode: string, - projectCode: string, + selectedPackageId: number, subclassRemark: string = "", subclass: string = "", ) { try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } - const projectId = project.id + const projectId = packageInfo[0].projectId; // 2. 올바른 projectId를 사용하여 tagSubfields 조회 const rows = await db @@ -1581,314 +1623,29 @@ export interface TagTypeOption { label: string; // tagTypes.description 값 } -export async function getProjectIdFromContractItemId( - projectCode: string -): Promise<number | null> { +export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> { try { // First get the contractId from contractItems - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), + const contractItem = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, contractItemId), columns: { - id: true + contractId: true } }); - if (!project) return null; - - return project?.id || null; - } catch (error) { - console.error("Error fetching projectId:", error); - return null; - } -} - -const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - + if (!contractItem) return null; -/** - * Engineering 폼 목록 가져오기 - */ -export async function getEngineeringForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 eng=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.eng, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = await fetch( - `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TOOL_ID: "eVCP" - }) - } - ) - - if (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = await fetch( - `${SEDP_API_BASE_URL}/Register/GetByID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TYPE_ID: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: true, - im: false - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue - } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 Engineering 폼을 DB에 저장했습니다.`) - } - - return formInfos - } catch (error) { - console.error("Engineering 폼 가져오기 실패:", error) - throw new Error("Failed to fetch engineering forms") - } -} - -/** - * IM 폼 목록 가져오기 - */ -export async function getIMForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 im=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.im, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = await fetch( - `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TOOL_ID: "eVCP" - }) - } - ) - - if (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = await fetch( - `${SEDP_API_BASE_URL}/Register/GetByID`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'accept': '*/*', - 'ApiKey': apiKey, - 'ProjectNo': projectCode - }, - body: JSON.stringify({ - ProjectNo: projectCode, - TYPE_ID: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: false, - im: true - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue + // Then get the projectId from contracts + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.id, contractItem.contractId), + columns: { + projectId: true } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 IM 폼을 DB에 저장했습니다.`) - } + }); - return formInfos + return contract?.projectId || null; } catch (error) { - console.error("IM 폼 가져오기 실패:", error) - throw new Error("Failed to fetch IM forms") + console.error("Error fetching projectId:", error); + return null; } }
\ No newline at end of file diff --git a/lib/tags-plant/table/add-tag-dialog.tsx b/lib/tags-plant/table/add-tag-dialog.tsx index 41731f63..9c82bf1a 100644 --- a/lib/tags-plant/table/add-tag-dialog.tsx +++ b/lib/tags-plant/table/add-tag-dialog.tsx @@ -61,7 +61,7 @@ import { getClassOptions, type ClassOption, TagTypeOption, -} from "@/lib/tags-plant/service" +} from "@/lib/tags/service" import { ScrollArea } from "@/components/ui/scroll-area" // Updated to support multiple rows and subclass @@ -98,11 +98,10 @@ interface UpdatedClassOption extends ClassOption { } interface AddTagDialogProps { - projectCode: string - packageCode: string + selectedPackageId: number } -export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { +export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const router = useRouter() const params = useParams() const lng = (params?.lng as string) || "ko" @@ -126,6 +125,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { const fieldIdsRef = React.useRef<Record<string, string>>({}) const classOptionIdsRef = React.useRef<Record<string, string>>({}) + console.log(selectedPackageId, "tag") // --------------- // Load Class Options (서브클래스 정보 포함) @@ -135,7 +135,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingClasses(true) try { // getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정 - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error(t("toast.classOptionsLoadFailed")) @@ -147,7 +147,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { if (open) { loadClassOptions() } - }, [open, projectCode, packageCode]) + }, [open, selectedPackageId]) // --------------- // react-hook-form with fieldArray support for multiple rows @@ -176,7 +176,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingSubFields(true) try { // 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가) - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark, subclass) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -313,7 +313,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { // Submit handler for multiple tags (서브클래스 정보 포함) // --------------- async function onSubmit(data: MultiTagFormValues) { - if (!projectCode) { + if (!selectedPackageId) { toast.error(t("toast.noSelectedPackageId")); return; } @@ -343,7 +343,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { }; try { - const res = await createTag(tagData, projectCode, packageCode); + const res = await createTag(tagData, selectedPackageId); if ("error" in res) { console.log(res.error) failedTags.push({ tag: row.tagNo, error: res.error }); diff --git a/lib/tags-plant/table/delete-tags-dialog.tsx b/lib/tags-plant/table/delete-tags-dialog.tsx index 69a4f4a6..6a024cda 100644 --- a/lib/tags-plant/table/delete-tags-dialog.tsx +++ b/lib/tags-plant/table/delete-tags-dialog.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { type Row } from "@tanstack/react-table" import { Loader, Trash } from "lucide-react" import { toast } from "sonner" + import { useMediaQuery } from "@/hooks/use-media-query" import { Button } from "@/components/ui/button" import { @@ -26,15 +27,15 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import { removeTags } from "@/lib//tags-plant/service" + +import { removeTags } from "@/lib//tags/service" import { Tag } from "@/db/schema/vendorData" interface DeleteTasksDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { tags: Row<Tag>["original"][] showTrigger?: boolean - projectCode: string - packageCode: string + selectedPackageId: number onSuccess?: () => void } @@ -42,8 +43,7 @@ export function DeleteTagsDialog({ tags, showTrigger = true, onSuccess, - projectCode, - packageCode, + selectedPackageId, ...props }: DeleteTasksDialogProps) { const [isDeletePending, startDeleteTransition] = React.useTransition() @@ -52,7 +52,7 @@ export function DeleteTagsDialog({ function onDelete() { startDeleteTransition(async () => { const { error } = await removeTags({ - ids: tags.map((tag) => tag.id),projectCode, packageCode + ids: tags.map((tag) => tag.id),selectedPackageId }) if (error) { diff --git a/lib/tags-plant/table/tag-table.tsx b/lib/tags-plant/table/tag-table.tsx index 2fdcd5fc..1986d933 100644 --- a/lib/tags-plant/table/tag-table.tsx +++ b/lib/tags-plant/table/tag-table.tsx @@ -1,4 +1,3 @@ -// components/vendor-data-plant/tags-table.tsx "use client" import * as React from "react" @@ -7,177 +6,40 @@ import type { DataTableFilterField, DataTableRowAction, } from "@/types/table" -import { useRouter } from "next/navigation" -import { toast } from "sonner" -import { Trash2, Download, Upload, Loader2, RefreshCcw, Plus } from "lucide-react" -import ExcelJS from "exceljs" -import type { Table as TanstackTable } from "@tanstack/react-table" -import { ClientDataTable } from "@/components/client-data-table/data-table" +import { toSentenceCase } from "@/lib/utils" +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + import { getColumns } from "./tag-table-column" import { Tag } from "@/db/schema/vendorData" import { DeleteTagsDialog } from "./delete-tags-dialog" +import { TagsTableToolbarActions } from "./tags-table-toolbar-actions" +import { TagsTableFloatingBar } from "./tags-table-floating-bar" +import { getTags } from "../service" import { UpdateTagSheet } from "./update-tag-sheet" -import { AddTagDialog } from "./add-tag-dialog" import { useAtomValue } from 'jotai' import { selectedModeAtom } from '@/atoms' -import { Skeleton } from "@/components/ui/skeleton" -import type { ColumnDef } from "@tanstack/react-table" -import { createDynamicAttributeColumns } from "../column-builder.service" -import { getAllTagsPlant, getUniqueAttributeKeys } from "../queries" -import { Button } from "@/components/ui/button" -import { exportTagsToExcel } from "./tags-export" -import { - bulkCreateTags, - getClassOptions, - getProjectIdFromContractItemId, - getSubfieldsByTagType -} from "../service" -import { decryptWithServerAction } from "@/components/drm/drmUtils" +// 여기서 받은 `promises`로부터 태그 목록을 가져와 상태를 세팅 +// 예: "selectedPackageId"는 props로 전달 interface TagsTableProps { - projectCode: string - packageCode: string -} - -// 태그 넘버링 룰 인터페이스 (Import용) -interface TagNumberingRule { - attributesId: string; - attributesDescription: string; - expression: string | null; - delimiter: string | null; - sortOrder: number; -} - -interface ClassOption { - code: string; - label: string; - tagTypeCode: string; - tagTypeDescription: string; -} - -interface SubFieldDef { - name: string; - label: string; - type: "select" | "text"; - options?: { value: string; label: string }[]; - expression?: string; - delimiter?: string; + promises: Promise< [ Awaited<ReturnType<typeof getTags>> ] > + selectedPackageId: number } -export function TagsTable({ - projectCode, - packageCode, -}: TagsTableProps) { - const router = useRouter() +export function TagsTable({ promises, selectedPackageId }: TagsTableProps) { + // 1) 데이터를 가져옴 (server component -> use(...) pattern) + const [{ data, pageCount }] = React.use(promises) const selectedMode = useAtomValue(selectedModeAtom) - // 상태 관리 - const [tableData, setTableData] = React.useState<Tag[]>([]) - const [columns, setColumns] = React.useState<ColumnDef<Tag>[]>([]) - const [isLoading, setIsLoading] = React.useState(true) const [rowAction, setRowAction] = React.useState<DataTableRowAction<Tag> | null>(null) - - // 선택된 행 관리 - const [selectedRowsData, setSelectedRowsData] = React.useState<Tag[]>([]) - const [clearSelection, setClearSelection] = React.useState(false) - - // 다이얼로그 상태 - const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) - const [deleteTarget, setDeleteTarget] = React.useState<Tag[]>([]) - const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false) - - // Import/Export 상태 - const [isPending, setIsPending] = React.useState(false) - const [isExporting, setIsExporting] = React.useState(false) - const fileInputRef = React.useRef<HTMLInputElement>(null) - - // Sync 상태 - const [isSyncing, setIsSyncing] = React.useState(false) - const [syncId, setSyncId] = React.useState<string | null>(null) - const pollingRef = React.useRef<NodeJS.Timeout | null>(null) - - // Table ref for export - const tableRef = React.useRef<TanstackTable<Tag> | null>(null) - - // Cache for validation - const [classOptions, setClassOptions] = React.useState<ClassOption[]>([]) - const [subfieldCache, setSubfieldCache] = React.useState<Record<string, SubFieldDef[]>>({}) - const [projectId, setProjectId] = React.useState<number | null>(null) - // Load project ID - React.useEffect(() => { - const fetchProjectId = async () => { - if (packageCode && projectCode) { - try { - const pid = await getProjectIdFromContractItemId(projectCode) - setProjectId(pid) - } catch (error) { - console.error("Failed to fetch project ID:", error) - } - } - } - fetchProjectId() - }, [projectCode]) - - // Load class options - React.useEffect(() => { - const loadClassOptions = async () => { - try { - const options = await getClassOptions(packageCode, projectCode) - setClassOptions(options) - } catch (error) { - console.error("Failed to load class options:", error) - } - } - loadClassOptions() - }, [packageCode, projectCode]) - - // 데이터 및 컬럼 로드 - React.useEffect(() => { - async function loadTableData() { - try { - setIsLoading(true) - - const [tagsData, attributeKeys] = await Promise.all([ - getAllTagsPlant(projectCode, packageCode), - getUniqueAttributeKeys(projectCode, packageCode), - ]) - - const baseColumns = getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - }) - - let dynamicColumns: ColumnDef<Tag>[] = [] - if (attributeKeys.length > 0) { - dynamicColumns = createDynamicAttributeColumns(attributeKeys) - } - - const actionsColumn = baseColumns.pop() - const finalColumns = [ - ...baseColumns, - ...dynamicColumns, - actionsColumn - ].filter(Boolean) as ColumnDef<Tag>[] - - setTableData(tagsData) - setColumns(finalColumns) - } catch (error) { - console.error("Error loading table data:", error) - toast.error("Failed to load table data") - setTableData([]) - setColumns(getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - })) - } finally { - setIsLoading(false) - } - } - - loadTableData() - }, [projectCode, packageCode]) + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) // Filter fields const filterFields: DataTableFilterField<Tag>[] = [ @@ -205,11 +67,6 @@ export function TagsTable({ type: "text", }, { - id: "class", - label: "Class", - type: "text", - }, - { id: "createdAt", label: "Created at", type: "date", @@ -221,562 +78,78 @@ export function TagsTable({ }, ] - // 선택된 행 개수 - const selectedRowCount = React.useMemo(() => { - return selectedRowsData.length - }, [selectedRowsData]) - - // 개별 행 삭제 - const handleDeleteRow = React.useCallback((rowData: Tag) => { - setDeleteTarget([rowData]) - setDeleteDialogOpen(true) - }, []) - - // 배치 삭제 - const handleBatchDelete = React.useCallback(() => { - if (selectedRowsData.length === 0) { - toast.error("삭제할 항목을 선택해주세요.") - return - } - setDeleteTarget(selectedRowsData) - setDeleteDialogOpen(true) - }, [selectedRowsData]) - - // 삭제 성공 후 처리 - const handleDeleteSuccess = React.useCallback(() => { - const tagNosToDelete = deleteTarget - .map(item => item.tagNo) - .filter(Boolean) - - setTableData(prev => - prev.filter(item => !tagNosToDelete.includes(item.tagNo)) - ) - - setSelectedRowsData([]) - setClearSelection(prev => !prev) - setDeleteTarget([]) - - toast.success("삭제되었습니다.") - }, [deleteTarget]) - - // 클래스 라벨로 태그 타입 코드 찾기 - const getTagTypeCodeByClassLabel = React.useCallback((classLabel: string): string | null => { - const classOption = classOptions.find(opt => opt.label === classLabel) - return classOption?.tagTypeCode || null - }, [classOptions]) - - // 태그 타입에 따른 서브필드 가져오기 - const fetchSubfieldsByTagType = React.useCallback(async (tagTypeCode: string): Promise<SubFieldDef[]> => { - if (subfieldCache[tagTypeCode]) { - return subfieldCache[tagTypeCode] - } - - try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, "", "") - const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ - name: field.name, - label: field.label, - type: field.type, - options: field.options || [], - expression: field.expression ?? undefined, - delimiter: field.delimiter ?? undefined, - })) - - setSubfieldCache(prev => ({ - ...prev, - [tagTypeCode]: formattedSubFields - })) - - return formattedSubFields - } catch (error) { - console.error(`Error fetching subfields for tagType ${tagTypeCode}:`, error) - return [] - } - }, [subfieldCache, projectCode]) - - // Class 기반 태그 번호 형식 검증 - const validateTagNumberByClass = React.useCallback(async ( - tagNo: string, - classLabel: string - ): Promise<string> => { - if (!tagNo) return "Tag number is empty." - if (!classLabel) return "Class is empty." - - try { - const tagTypeCode = getTagTypeCodeByClassLabel(classLabel) - if (!tagTypeCode) { - return `No tag type found for class '${classLabel}'.` - } - - const subfields = await fetchSubfieldsByTagType(tagTypeCode) - if (!subfields || subfields.length === 0) { - return `No subfields found for tag type code '${tagTypeCode}'.` - } - - let remainingTagNo = tagNo - - for (const field of subfields) { - const delimiter = field.delimiter || "" - let nextDelimiterPos - - if (delimiter && remainingTagNo.includes(delimiter)) { - nextDelimiterPos = remainingTagNo.indexOf(delimiter) - } else { - nextDelimiterPos = remainingTagNo.length - } - - const part = remainingTagNo.substring(0, nextDelimiterPos) - - if (!part) { - return `Empty part for field '${field.label}'.` - } - - if (field.expression) { - try { - let cleanPattern = field.expression.replace(/^\^/, '').replace(/\$$/, '') - const regex = new RegExp(`^${cleanPattern}$`) - - if (!regex.test(part)) { - return `Part '${part}' for field '${field.label}' does not match the pattern '${field.expression}'.` - } - } catch (error) { - console.error(`Invalid regex pattern: ${field.expression}`, error) - return `Invalid pattern for field '${field.label}': ${field.expression}` - } - } - - if (field.type === "select" && field.options && field.options.length > 0) { - const validValues = field.options.map(opt => opt.value) - if (!validValues.includes(part)) { - return `'${part}' is not a valid value for field '${field.label}'. Valid options: ${validValues.join(", ")}.` - } - } - - if (delimiter && nextDelimiterPos < remainingTagNo.length) { - remainingTagNo = remainingTagNo.substring(nextDelimiterPos + delimiter.length) - } else { - remainingTagNo = "" - break - } - } - - if (remainingTagNo) { - return `Tag number has extra parts: '${remainingTagNo}'.` - } - - return "" - } catch (error) { - console.error("Error validating tag number by class:", error) - return "Error validating tag number format." - } - }, [getTagTypeCodeByClassLabel, fetchSubfieldsByTagType]) - - // Import 파일 선택 - const handleImportClick = () => { - fileInputRef.current?.click() - } - - // Import 파일 처리 - const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { - const file = e.target.files?.[0] - if (!file) return - - e.target.value = "" - setIsPending(true) - - try { - const workbook = new ExcelJS.Workbook() - const arrayBuffer = await decryptWithServerAction(file) - await workbook.xlsx.load(arrayBuffer) - - const worksheet = workbook.worksheets[0] - const lastColIndex = worksheet.columnCount + 1 - worksheet.getRow(1).getCell(lastColIndex).value = "Error" - - const headerRowValues = worksheet.getRow(1).values as ExcelJS.CellValue[] - - // Excel header to accessor mapping - const excelHeaderToAccessor: Record<string, string> = {} - for (const col of columns) { - const meta = col.meta as { excelHeader?: string } | undefined - if (meta?.excelHeader) { - const accessor = col.id as string - excelHeaderToAccessor[meta.excelHeader] = accessor - } - } - - const accessorIndexMap: Record<string, number> = {} - for (let i = 1; i < headerRowValues.length; i++) { - const cellVal = String(headerRowValues[i] ?? "").trim() - if (!cellVal) continue - const accessor = excelHeaderToAccessor[cellVal] - if (accessor) { - accessorIndexMap[accessor] = i - } - } - - let errorCount = 0 - const importedRows: Tag[] = [] - const fileTagNos = new Set<string>() - const lastRow = worksheet.lastRow?.number || 1 - - for (let rowNum = 2; rowNum <= lastRow; rowNum++) { - const row = worksheet.getRow(rowNum) - const rowVals = row.values as ExcelJS.CellValue[] - if (!rowVals || rowVals.length <= 1) continue - - let errorMsg = "" - - const tagNoIndex = accessorIndexMap["tagNo"] - const classIndex = accessorIndexMap["class"] - - const tagNo = tagNoIndex ? String(rowVals[tagNoIndex] ?? "").trim() : "" - const classVal = classIndex ? String(rowVals[classIndex] ?? "").trim() : "" - - if (!tagNo) { - errorMsg += `Tag No is empty. ` - } - if (!classVal) { - errorMsg += `Class is empty. ` - } - - if (tagNo) { - const dup = tableData.find(t => t.tagNo === tagNo) - if (dup) { - errorMsg += `TagNo '${tagNo}' already exists. ` - } - - if (fileTagNos.has(tagNo)) { - errorMsg += `TagNo '${tagNo}' is duplicated within this file. ` - } else { - fileTagNos.add(tagNo) - } - } - - if (tagNo && classVal && !errorMsg) { - const classValidationError = await validateTagNumberByClass(tagNo, classVal) - if (classValidationError) { - errorMsg += classValidationError + " " - } - } - - if (errorMsg) { - row.getCell(lastColIndex).value = errorMsg.trim() - errorCount++ - } else { - const finalTagType = getTagTypeCodeByClassLabel(classVal) ?? "" - - importedRows.push({ - id: 0, - packageCode: packageCode, - projectCode: projectCode, - formId: null, - tagNo, - tagType: finalTagType, - class: classVal, - description: String(rowVals[accessorIndexMap["description"] ?? 0] ?? "").trim(), - createdAt: new Date(), - updatedAt: new Date(), - }) - } - } - - if (errorCount > 0) { - const outBuf = await workbook.xlsx.writeBuffer() - const errorFile = new Blob([outBuf]) - const url = URL.createObjectURL(errorFile) - const link = document.createElement("a") - link.href = url - link.download = "tag_import_errors.xlsx" - link.click() - URL.revokeObjectURL(url) - - toast.error(`There are ${errorCount} error row(s). Please see downloaded file.`) - return - } - - if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode) - if ("error" in result) { - toast.error(result.error) - } else { - toast.success(`${result.data.createdCount}개의 태그가 성공적으로 생성되었습니다.`) - router.refresh() - } - } - } catch (err) { - console.error(err) - toast.error("파일 업로드 중 오류가 발생했습니다.") - } finally { - setIsPending(false) - } - } - - // Export 함수 - const handleExport = async () => { - if (!tableRef.current) { - toast.error("테이블이 준비되지 않았습니다.") - return - } - - try { - setIsExporting(true) - await exportTagsToExcel(tableRef.current, packageCode, projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, - excludeColumns: ["select", "actions", "createdAt", "updatedAt"], - }) - toast.success("태그 목록이 성공적으로 내보내졌습니다.") - } catch (error) { - console.error("Export error:", error) - toast.error("태그 목록 내보내기 중 오류가 발생했습니다.") - } finally { - setIsExporting(false) - } - } - - // Sync 함수 - const startGetTags = async () => { - try { - setIsSyncing(true) - - const response = await fetch('/api/cron/tags-plant/start', { - method: 'POST', - body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, - mode: selectedMode - }) - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to start tag import') - } - - const data = await response.json() - - if (data.syncId) { - setSyncId(data.syncId) - toast.info('Tag import started. This may take a while...') - startPolling(data.syncId) - } else { - throw new Error('No import ID returned from server') - } - } catch (error) { - console.error('Error starting tag import:', error) - toast.error( - error instanceof Error - ? error.message - : 'An error occurred while starting tag import' - ) - setIsSyncing(false) - } - } - - const startPolling = (id: string) => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - - pollingRef.current = setInterval(async () => { - try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) - - if (!response.ok) { - throw new Error('Failed to get tag import status') - } - - const data = await response.json() - - if (data.status === 'completed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } - - router.refresh() - setIsSyncing(false) - setSyncId(null) + // 3) useDataTable 훅으로 react-table 구성 + const { table } = useDataTable({ + data: data, // <-- 여기서 tableData 사용 + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + // sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", - toast.success( - `Tags imported successfully! ${data.result?.processedCount || 0} items processed.` - ) - } else if (data.status === 'failed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } + }) - setIsSyncing(false) - setSyncId(null) - toast.error(data.error || 'Import failed') - } - } catch (error) { - console.error('Error checking importing status:', error) - } - }, 5000) - } + const [isCompact, setIsCompact] = React.useState<boolean>(false) - // rowAction 처리 - React.useEffect(() => { - if (rowAction?.type === "delete") { - handleDeleteRow(rowAction.row.original) - setRowAction(null) - } - }, [rowAction, handleDeleteRow]) - // Cleanup - React.useEffect(() => { - return () => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - } + const handleCompactChange = React.useCallback((compact: boolean) => { + setIsCompact(compact) }, []) - - // 로딩 중 - if (isLoading) { - return ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-[500px] w-full" /> - <Skeleton className="h-10 w-full" /> - </div> - ) - } - + + return ( <> - <ClientDataTable - data={tableData} - columns={columns} - advancedFilterFields={advancedFilterFields} - autoSizeColumns - onSelectedRowsChange={setSelectedRowsData} - clearSelection={clearSelection} - onTableReady={(table) => { - tableRef.current = table - }} - > - <div className="flex items-center gap-2"> - {/* 삭제 버튼 - 선택된 항목이 있을 때만 */} - {selectedRowCount > 0 && ( - <Button - variant="destructive" - size="sm" - onClick={handleBatchDelete} - > - <Trash2 className="mr-2 size-4" /> - Delete ({selectedRowCount}) - </Button> - )} - - {/* Get Tags 버튼 */} - <Button - variant="samsung" - size="sm" - onClick={startGetTags} - disabled={isSyncing} - > - <RefreshCcw className={`size-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} /> - <span className="hidden sm:inline"> - {isSyncing ? 'Syncing...' : 'Get Tags'} - </span> - </Button> - - {/* Add Tag 버튼 */} - <AddTagDialog - projectCode={projectCode} - packageCode={packageCode}/> + <DataTable + table={table} + compact={isCompact} - {/* Import 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleImportClick} - disabled={isPending || isExporting} - > - {isPending ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Upload className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Import</span> - </Button> - - {/* Export 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleExport} - disabled={isPending || isExporting || !tableRef.current} - > - {isExporting ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Download className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Export</span> - </Button> - </div> - </ClientDataTable> - - {/* Hidden file input */} - <input - ref={fileInputRef} - type="file" - accept=".xlsx,.xls" - className="hidden" - onChange={handleFileChange} - /> + floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + enableCompactToggle={true} + compactStorageKey="tagTableCompact" + onCompactChange={handleCompactChange} + > + {/* + 4) ToolbarActions에 tableData, setTableData 넘겨서 + import 시 상태 병합 + */} + <TagsTableToolbarActions + table={table} + selectedPackageId={selectedPackageId} + tableData={data} // <-- pass current data + selectedMode={selectedMode} + /> + </DataTableAdvancedToolbar> + </DataTable> - {/* Update Sheet */} <UpdateTagSheet open={rowAction?.type === "update"} - onOpenChange={(open) => { - if (!open) setRowAction(null) - }} + onOpenChange={() => setRowAction(null)} tag={rowAction?.row.original ?? null} - packageCode={packageCode} - projectCode={projectCode} - onUpdateSuccess={(updatedValues) => { - if (rowAction?.row.original?.tagNo) { - const tagNo = rowAction.row.original.tagNo - setTableData(prev => - prev.map(item => - item.tagNo === tagNo ? updatedValues : item - ) - ) - } - }} + selectedPackageId={selectedPackageId} /> - {/* Delete Dialog */} + <DeleteTagsDialog - tags={deleteTarget} - packageCode={packageCode} - projectCode={projectCode} - open={deleteDialogOpen} - onOpenChange={(open) => { - if (!open) { - setDeleteDialogOpen(false) - setDeleteTarget([]) - } - }} - onSuccess={handleDeleteSuccess} + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + tags={rowAction?.row.original ? [rowAction?.row.original] : []} showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + selectedPackageId={selectedPackageId} /> - - {/* Add Tag Dialog */} - {/* <AddTagDialog - projectCode={projectCode} - packageCode={packageCode} - open={addTagDialogOpen} - onOpenChange={setAddTagDialogOpen} - onSuccess={() => { - router.refresh() - }} - /> */} </> ) }
\ No newline at end of file diff --git a/lib/tags-plant/table/tags-export.tsx b/lib/tags-plant/table/tags-export.tsx index a3255a0b..fa85148d 100644 --- a/lib/tags-plant/table/tags-export.tsx +++ b/lib/tags-plant/table/tags-export.tsx @@ -15,8 +15,7 @@ import { getClassOptions } from "../service" */ export async function exportTagsToExcel( table: Table<Tag>, - packageCode: string, - projectCode: string, + selectedPackageId: number, { filename = "Tags", excludeColumns = ["select", "actions", "createdAt", "updatedAt"], @@ -43,7 +42,7 @@ export async function exportTagsToExcel( const worksheet = workbook.addWorksheet("Tags") // 3. Tag Class 옵션 가져오기 - const classOptions = await getClassOptions(packageCode, projectCode) + const classOptions = await getClassOptions(selectedPackageId) // 4. 유효성 검사 시트 생성 const validationSheet = workbook.addWorksheet("ValidationData") diff --git a/lib/tags-plant/table/tags-table-floating-bar.tsx b/lib/tags-plant/table/tags-table-floating-bar.tsx index eadbfb12..8d55b7ac 100644 --- a/lib/tags-plant/table/tags-table-floating-bar.tsx +++ b/lib/tags-plant/table/tags-table-floating-bar.tsx @@ -36,13 +36,12 @@ import { Tag } from "@/db/schema/vendorData" interface TagsTableFloatingBarProps { table: Table<Tag> - packageCode: string - projectCode: string + selectedPackageId: number } -export function TagsTableFloatingBar({ table, packageCode, projectCode}: TagsTableFloatingBarProps) { +export function TagsTableFloatingBar({ table, selectedPackageId }: TagsTableFloatingBarProps) { const rows = table.getFilteredSelectedRowModel().rows const [isPending, startTransition] = React.useTransition() diff --git a/lib/tags-plant/table/tags-table-toolbar-actions.tsx b/lib/tags-plant/table/tags-table-toolbar-actions.tsx index c80a600e..cc2d82b4 100644 --- a/lib/tags-plant/table/tags-table-toolbar-actions.tsx +++ b/lib/tags-plant/table/tags-table-toolbar-actions.tsx @@ -52,8 +52,7 @@ interface TagsTableToolbarActionsProps { /** react-table 객체 */ table: Table<Tag> /** 현재 선택된 패키지 ID */ - packageCode: string - projectCode: string + selectedPackageId: number /** 현재 태그 목록(상태) */ tableData: Tag[] /** 태그 목록을 갱신하는 setState */ @@ -69,8 +68,7 @@ interface TagsTableToolbarActionsProps { */ export function TagsTableToolbarActions({ table, - packageCode, - projectCode, + selectedPackageId, tableData, selectedMode }: TagsTableToolbarActionsProps) { @@ -96,7 +94,7 @@ export function TagsTableToolbarActions({ React.useEffect(() => { const loadClassOptions = async () => { try { - const options = await getClassOptions(packageCode, projectCode) + const options = await getClassOptions(selectedPackageId) setClassOptions(options) } catch (error) { console.error("Failed to load class options:", error) @@ -104,7 +102,7 @@ export function TagsTableToolbarActions({ } loadClassOptions() - }, [packageCode, projectCode]) + }, [selectedPackageId]) // 숨겨진 <input>을 클릭 function handleImportClick() { @@ -137,11 +135,12 @@ export function TagsTableToolbarActions({ const [projectId, setProjectId] = React.useState<number | null>(null); + // Add useEffect to fetch projectId when selectedPackageId changes React.useEffect(() => { const fetchProjectId = async () => { - if (packageCode && projectCode) { + if (selectedPackageId) { try { - const pid = await getProjectIdFromContractItemId(projectCode ); + const pid = await getProjectIdFromContractItemId(selectedPackageId); setProjectId(pid); } catch (error) { console.error("Failed to fetch project ID:", error); @@ -151,7 +150,7 @@ export function TagsTableToolbarActions({ }; fetchProjectId(); - }, [projectCode]); + }, [selectedPackageId]); // 특정 attributesId에 대한 옵션 가져오기 const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => { @@ -196,7 +195,7 @@ export function TagsTableToolbarActions({ } try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) // API 응답을 SubFieldDef 형식으로 변환 const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ @@ -479,7 +478,7 @@ export function TagsTableToolbarActions({ if (tagNo) { // 이미 tableData 내 존재 여부 const dup = tableData.find( - (t) => t.tagNo === tagNo + (t) => t.contractItemId === selectedPackageId && t.tagNo === tagNo ) if (dup) { errorMsg += `TagNo '${tagNo}' already exists. ` @@ -524,8 +523,7 @@ export function TagsTableToolbarActions({ // 정상 행을 importedRows에 추가 importedRows.push({ id: 0, // 임시 - packageCode: packageCode, - projectCode: projectCode, + contractItemId: selectedPackageId, formId: null, tagNo, tagType: finalTagType, // ← 코드로 저장할지, Description으로 저장할지 결정 @@ -554,7 +552,7 @@ export function TagsTableToolbarActions({ // 정상 행이 있으면 태그 생성 요청 if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode); + const result = await bulkCreateTags(importedRows, selectedPackageId); if ("error" in result) { toast.error(result.error); } else { @@ -577,8 +575,8 @@ export function TagsTableToolbarActions({ setIsExporting(true) // 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출 - await exportTagsToExcel(table, packageCode,projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, + await exportTagsToExcel(table, selectedPackageId, { + filename: `Tags_${selectedPackageId}`, excludeColumns: ["select", "actions", "createdAt", "updatedAt"], }) @@ -596,11 +594,10 @@ export function TagsTableToolbarActions({ setIsLoading(true) // API 엔드포인트 호출 - 작업 시작만 요청 - const response = await fetch('/api/cron/tags-plant/start', { + const response = await fetch('/api/cron/tags/start', { method: 'POST', body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, + packageId: selectedPackageId, mode: selectedMode // 모드 정보 추가 }) }) @@ -641,7 +638,7 @@ export function TagsTableToolbarActions({ // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) + const response = await fetch(`/api/cron/tags/status?id=${id}`) if (!response.ok) { throw new Error('Failed to get tag import status') @@ -702,8 +699,7 @@ export function TagsTableToolbarActions({ .getFilteredSelectedRowModel() .rows.map((row) => row.original)} onSuccess={() => table.toggleAllRowsSelected(false)} - projectCode={projectCode} - packageCode={packageCode} + selectedPackageId={selectedPackageId} /> ) : null} <Button @@ -719,7 +715,7 @@ export function TagsTableToolbarActions({ </span> </Button> - <AddTagDialog projectCode={projectCode} packageCode={packageCode} /> + <AddTagDialog selectedPackageId={selectedPackageId} /> {/* Import */} <Button diff --git a/lib/tags-plant/table/update-tag-sheet.tsx b/lib/tags-plant/table/update-tag-sheet.tsx index 2be1e732..613abaa9 100644 --- a/lib/tags-plant/table/update-tag-sheet.tsx +++ b/lib/tags-plant/table/update-tag-sheet.tsx @@ -50,7 +50,7 @@ import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { Tag } from "@/db/schema/vendorData" -import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags-plant/service" +import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags/service" // SubFieldDef 인터페이스 interface SubFieldDef { @@ -84,11 +84,10 @@ type UpdateTagSchema = z.infer<typeof updateTagSchema> & Record<string, string> interface UpdateTagSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { tag: Tag | null - packageCode: string - projectCode: string + selectedPackageId: number } -export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: UpdateTagSheetProps) { +export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([]) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null) @@ -111,7 +110,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat setIsLoadingClasses(true) try { - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error("클래스 옵션을 불러오는데 실패했습니다.") @@ -165,7 +164,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -222,7 +221,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat ), } - const result = await updateTag(tagData, projectCode,packageCode ) + const result = await updateTag(tagData, selectedPackageId) if ("error" in result) { toast.error(result.error) |
