summaryrefslogtreecommitdiff
path: root/lib/tags-plant
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
commit5870b73785715d1585531e655c06d8c068eb64ac (patch)
tree1d19e1482f5210cc56e778158b51e810f9717c46 /lib/tags-plant
parent95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff)
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
Diffstat (limited to 'lib/tags-plant')
-rw-r--r--lib/tags-plant/column-builder.service.ts34
-rw-r--r--lib/tags-plant/queries.ts68
-rw-r--r--lib/tags-plant/repository.ts42
-rw-r--r--lib/tags-plant/service.ts729
-rw-r--r--lib/tags-plant/table/add-tag-dialog.tsx18
-rw-r--r--lib/tags-plant/table/delete-tags-dialog.tsx12
-rw-r--r--lib/tags-plant/table/tag-table.tsx775
-rw-r--r--lib/tags-plant/table/tags-export.tsx5
-rw-r--r--lib/tags-plant/table/tags-table-floating-bar.tsx5
-rw-r--r--lib/tags-plant/table/tags-table-toolbar-actions.tsx42
-rw-r--r--lib/tags-plant/table/update-tag-sheet.tsx13
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)