summaryrefslogtreecommitdiff
path: root/lib/tags
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tags')
-rw-r--r--lib/tags/service.ts337
-rw-r--r--lib/tags/table/add-tag-dialog.tsx32
-rw-r--r--lib/tags/table/tag-table.tsx18
-rw-r--r--lib/tags/table/tags-export.tsx5
-rw-r--r--lib/tags/table/tags-table-toolbar-actions.tsx178
-rw-r--r--lib/tags/table/update-tag-sheet.tsx3
6 files changed, 450 insertions, 123 deletions
diff --git a/lib/tags/service.ts b/lib/tags/service.ts
index 8477b1fb..b02f5dc2 100644
--- a/lib/tags/service.ts
+++ b/lib/tags/service.ts
@@ -7,7 +7,7 @@ import { createTagSchema, GetTagsSchema, updateTagSchema, UpdateTagSchema, type
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 { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, sql, ne, count, isNull } from "drizzle-orm";
import { countTags, insertTag, selectTags } from "./repository";
import { getErrorMessage } from "../handle-error";
import { getFormMappingsByTagType } from './form-mapping-service';
@@ -29,7 +29,7 @@ export async function getTags(input: GetTagsSchema, packagesId: number) {
try {
const offset = (input.page - 1) * input.perPage;
- // (1) advancedWhere
+ // (1) advancedWhere
const advancedWhere = filterColumns({
table: tags,
filters: input.filters,
@@ -110,14 +110,14 @@ export async function createTag(
return await db.transaction(async (tx) => {
// 1) 선택된 contractItem의 contractId 가져오기
const contractItemResult = await tx
- .select({
- contractId: contractItems.contractId,
- projectId: contracts.projectId // projectId 추가
- })
- .from(contractItems)
- .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인
- .where(eq(contractItems.id, selectedPackageId))
- .limit(1)
+ .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" }
@@ -160,7 +160,7 @@ export async function createTag(
projectId
)
}
-
+
// 4) 이 태그 타입에 대한 주요 폼(첫 번째 폼)을 찾거나 생성
let primaryFormId: number | null = null
@@ -199,6 +199,7 @@ export async function createTag(
contractItemId: selectedPackageId,
formCode: formMapping.formCode,
formName: formMapping.formName,
+ im: true
})
.returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName })
@@ -253,6 +254,125 @@ export async function createTag(
}
}
+export async function createTagInForm(
+ formData: CreateTagSchema,
+ selectedPackageId: number | null,
+ formCode: string
+) {
+ if (!selectedPackageId) {
+ return { error: "No selectedPackageId provided" }
+ }
+
+ // Validate formData
+ const validated = createTagSchema.safeParse(formData)
+ if (!validated.success) {
+ return { error: validated.error.flatten().formErrors.join(", ") }
+ }
+
+ // React 서버 액션에서 매 요청마다 실행
+ unstable_noStore()
+
+ try {
+ // 하나의 트랜잭션에서 모든 작업 수행
+ return await db.transaction(async (tx) => {
+ // 1) 선택된 contractItem의 contractId 가져오기
+ const contractItemResult = await tx
+ .select({
+ contractId: contractItems.contractId,
+ projectId: contracts.projectId // projectId 추가
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인
+ .where(eq(contractItems.id, selectedPackageId))
+ .limit(1)
+
+ if (contractItemResult.length === 0) {
+ return { error: "Contract item not found" }
+ }
+
+ const contractId = contractItemResult[0].contractId
+ const projectId = contractItemResult[0].projectId
+
+ // 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인
+ const duplicateCheck = await tx
+ .select({ count: sql<number>`count(*)` })
+ .from(tags)
+ .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id))
+ .where(
+ and(
+ eq(contractItems.contractId, contractId),
+ eq(tags.tagNo, validated.data.tagNo)
+ )
+ )
+
+ if (duplicateCheck[0].count > 0) {
+ return {
+ error: `태그 번호 "${validated.data.tagNo}"는 이미 이 계약 내에 존재합니다.`,
+ }
+ }
+
+ const form = await db.query.forms.findFirst({
+ where: eq(forms.formCode, formCode)
+ });
+
+ if (form?.id) {
+ // 5) 새 Tag 생성 (같은 트랜잭션 `tx` 사용)
+ const [newTag] = await insertTag(tx, {
+ contractItemId: selectedPackageId,
+ formId: form.id,
+ tagNo: validated.data.tagNo,
+ class: validated.data.class,
+ tagType: validated.data.tagType,
+ description: validated.data.description ?? null,
+ })
+
+ let updatedData: Array<{
+ TAG_NO: string;
+ TAG_DESC?: string;
+ }> = [];
+
+ updatedData.push({
+ TAG_NO: validated.data.tagNo,
+ TAG_DESC: validated.data.description ?? null,
+ });
+
+ const entry = await db.query.formEntries.findFirst({
+ where: and(
+ eq(formEntries.formCode, formCode),
+ eq(formEntries.contractItemId, selectedPackageId),
+ )
+ });
+
+ if (entry && entry.id && updatedData.length > 0) {
+ await db
+ .update(formEntries)
+ .set({ data: updatedData })
+ .where(eq(formEntries.id, entry.id));
+ }
+
+ console.log(`tags-${selectedPackageId}`, "create", newTag)
+
+ }
+
+ // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시)
+ revalidateTag(`tags-${selectedPackageId}`)
+ revalidateTag(`forms-${selectedPackageId}`)
+ revalidateTag("tags")
+
+ // 7) 성공 시 반환
+ return {
+ success: true,
+ data: null
+ }
+ })
+ } catch (err: any) {
+ console.log("createTag in Form error:", err)
+
+ console.error("createTag in Form error:", err)
+ return { error: getErrorMessage(err) }
+ }
+}
+
export async function updateTag(
formData: UpdateTagSchema & { id: number },
selectedPackageId: number | null
@@ -292,14 +412,14 @@ export async function updateTag(
// 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)
+ .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" }
@@ -330,8 +450,8 @@ export async function updateTag(
}
// 4) 태그 타입이나 클래스가 변경되었는지 확인
- const isTagTypeOrClassChanged =
- originalTag.tagType !== validated.data.tagType ||
+ const isTagTypeOrClassChanged =
+ originalTag.tagType !== validated.data.tagType ||
originalTag.class !== validated.data.class
let primaryFormId = originalTag.formId
@@ -459,17 +579,17 @@ export async function bulkCreateTags(
selectedPackageId: number
) {
unstable_noStore();
-
+
if (!tagsfromExcel.length) {
return { error: "No tags provided" };
}
-
+
try {
// 단일 트랜잭션으로 모든 작업 처리
return await db.transaction(async (tx) => {
// 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만)
const contractItemResult = await tx
- .select({
+ .select({
contractId: contractItems.contractId,
projectId: contracts.projectId // projectId 추가
})
@@ -477,14 +597,14 @@ export async function bulkCreateTags(
.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 contractId = contractItemResult[0].contractId;
const projectId = contractItemResult[0].projectId; // projectId 추출
-
+
// 2. 모든 태그 번호 중복 검사 (한 번에)
const tagNos = tagsfromExcel.map(tag => tag.tagNo);
const duplicateCheck = await tx
@@ -495,24 +615,24 @@ export async function bulkCreateTags(
eq(contractItems.contractId, contractId),
inArray(tags.tagNo, tagNos)
));
-
+
if (duplicateCheck.length > 0) {
return {
error: `태그 번호 "${duplicateCheck.map(d => d.tagNo).join(', ')}"는 이미 존재합니다.`
};
}
-
+
// 3. 태그별 폼 정보 처리 및 태그 생성
const createdTags = [];
const allFormsInfo = []; // 모든 태그에 대한 폼 정보 저장
// 태그 유형별 폼 매핑 캐싱 (성능 최적화)
const formMappingsCache = new Map();
-
+
for (const tagData of tagsfromExcel) {
// 캐시 키 생성 (tagType + class)
const cacheKey = `${tagData.tagType}|${tagData.class || 'NONE'}`;
-
+
// 폼 매핑 가져오기 (캐시 사용)
let formMappings;
if (formMappingsCache.has(cacheKey)) {
@@ -526,11 +646,11 @@ export async function bulkCreateTags(
);
formMappingsCache.set(cacheKey, formMappings);
}
-
+
// 폼 처리 로직
let primaryFormId: number | null = null;
const createdOrExistingForms: CreatedOrExistingForm[] = [];
-
+
if (formMappings && formMappings.length > 0) {
for (const formMapping of formMappings) {
// 해당 폼이 이미 존재하는지 확인
@@ -590,7 +710,7 @@ export async function bulkCreateTags(
projectId
);
}
-
+
// 태그 생성
const [newTag] = await insertTag(tx, {
contractItemId: selectedPackageId,
@@ -600,9 +720,9 @@ export async function bulkCreateTags(
tagType: tagData.tagType,
description: tagData.description || null,
});
-
+
createdTags.push(newTag);
-
+
// 해당 태그의 폼 정보 저장
allFormsInfo.push({
tagNo: tagData.tagNo,
@@ -610,12 +730,12 @@ export async function bulkCreateTags(
primaryFormId,
});
}
-
+
// 4. 캐시 무효화 (한 번만)
revalidateTag(`tags-${selectedPackageId}`);
revalidateTag(`forms-${selectedPackageId}`);
revalidateTag("tags");
-
+
return {
success: true,
data: {
@@ -644,33 +764,33 @@ function removeTagFromDataJson(
tagNo: string
): any {
// data 구조가 어떻게 생겼는지에 따라 로직이 달라집니다.
- // 예: data 배열 안에 { tagNumber: string, ... } 형태로 여러 객체가 있다고 가정
+ // 예: data 배열 안에 { TAG_NO: string, ... } 형태로 여러 객체가 있다고 가정
if (!Array.isArray(dataJson)) return dataJson
- return dataJson.filter((entry) => entry.tagNumber !== tagNo)
+ return dataJson.filter((entry) => entry.TAG_NO !== tagNo)
}
export async function removeTags(input: RemoveTagsInput) {
unstable_noStore() // React 서버 액션 무상태 함수
-
+
const { ids, selectedPackageId } = input
-
+
try {
await db.transaction(async (tx) => {
const packageInfo = await tx
- .select({
- projectId: contracts.projectId
- })
- .from(contractItems)
- .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
- .where(eq(contractItems.id, selectedPackageId))
- .limit(1);
-
- if (packageInfo.length === 0) {
- throw new Error(`Contract item with ID ${selectedPackageId} not found`);
- }
-
- const projectId = packageInfo[0].projectId;
+ .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
@@ -682,7 +802,7 @@ export async function removeTags(input: RemoveTagsInput) {
})
.from(tags)
.where(inArray(tags.id, ids))
-
+
// 2) 태그 타입과 클래스의 고유 조합 추출
const uniqueTypeClassCombinations = [...new Set(
tagsToDelete.map(tag => `${tag.tagType}|${tag.class || ''}`)
@@ -690,7 +810,7 @@ export async function removeTags(input: RemoveTagsInput) {
const [tagType, classValue] = combo.split('|');
return { tagType, class: classValue || undefined };
});
-
+
// 3) 각 태그 타입/클래스 조합에 대해 처리
for (const { tagType, class: classValue } of uniqueTypeClassCombinations) {
// 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인
@@ -705,18 +825,18 @@ export async function removeTags(input: RemoveTagsInput) {
eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인
)
)
-
+
// 3-2) 이 태그 타입/클래스에 연결된 폼 매핑 가져오기
- const formMappings = await getFormMappingsByTagType(tagType,projectId,classValue);
-
+ const formMappings = await getFormMappingsByTagType(tagType, projectId, classValue);
+
if (!formMappings.length) continue;
-
+
// 3-3) 이 태그 타입/클래스와 관련된 태그 번호 추출
const relevantTagNos = tagsToDelete
- .filter(tag => tag.tagType === tagType &&
- (classValue ? tag.class === classValue : !tag.class))
+ .filter(tag => tag.tagType === tagType &&
+ (classValue ? tag.class === classValue : !tag.class))
.map(tag => tag.tagNo);
-
+
// 3-4) 각 폼 코드에 대해 처리
for (const formMapping of formMappings) {
// 다른 태그가 없다면 폼 삭제
@@ -730,7 +850,7 @@ export async function removeTags(input: RemoveTagsInput) {
eq(forms.formCode, formMapping.formCode)
)
)
-
+
// formEntries 테이블에서도 해당 formCode 관련 데이터 삭제
await tx
.delete(formEntries)
@@ -740,7 +860,7 @@ export async function removeTags(input: RemoveTagsInput) {
eq(formEntries.formCode, formMapping.formCode)
)
)
- }
+ }
// 다른 태그가 있다면 formEntries 데이터에서 해당 태그 정보만 제거
else if (relevantTagNos.length > 0) {
const formEntryRecords = await tx
@@ -755,16 +875,16 @@ export async function removeTags(input: RemoveTagsInput) {
eq(formEntries.formCode, formMapping.formCode)
)
)
-
+
// 각 formEntry에 대해 처리
for (const entry of formEntryRecords) {
let updatedJson = entry.data;
-
+
// 각 tagNo에 대해 JSON 데이터에서 제거
for (const tagNo of relevantTagNos) {
updatedJson = removeTagFromDataJson(updatedJson, tagNo);
}
-
+
// 변경이 있다면 업데이트
await tx
.update(formEntries)
@@ -774,15 +894,15 @@ export async function removeTags(input: RemoveTagsInput) {
}
}
}
-
+
// 4) 마지막으로 tags 테이블에서 태그들 삭제
await tx.delete(tags).where(inArray(tags.id, ids))
})
-
+
// 5) 캐시 무효화
revalidateTag(`tags-${selectedPackageId}`)
revalidateTag(`forms-${selectedPackageId}`)
-
+
return { data: null, error: null }
} catch (err) {
return { data: null, error: getErrorMessage(err) }
@@ -802,7 +922,28 @@ export interface ClassOption {
* Class 옵션 목록을 가져오는 함수
* 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함
*/
-export async function getClassOptions(){
+export async function getClassOptions(packageId?: number) {
+ if (!packageId) {
+ throw new Error("패키지 ID가 필요합니다");
+ }
+
+ // First, get the projectId from the contract associated with the package
+ const packageInfo = await db
+ .select({
+ projectId: contracts.projectId
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contracts.id, contractItems.contractId))
+ .where(eq(contractItems.id, packageId))
+ .limit(1);
+
+ if (!packageInfo.length) {
+ throw new Error("패키지를 찾을 수 없거나 연결된 프로젝트가 없습니다");
+ }
+
+ const projectId = packageInfo[0].projectId;
+
+ // Now get the tag classes filtered by projectId
const rows = await db
.select({
id: tagClasses.id,
@@ -812,16 +953,19 @@ export async function getClassOptions(){
tagTypeDescription: tagTypes.description,
})
.from(tagClasses)
- .leftJoin(tagTypes, eq(tagTypes.code, tagClasses.tagTypeCode))
+ .leftJoin(tagTypes, and(
+ eq(tagTypes.code, tagClasses.tagTypeCode),
+ eq(tagTypes.projectId, tagClasses.projectId)
+ ))
+ .where(eq(tagClasses.projectId, projectId));
return rows.map((row) => ({
code: row.code,
label: row.label,
tagTypeCode: row.tagTypeCode,
tagTypeDescription: row.tagTypeDescription ?? "",
- }))
+ }));
}
-
interface SubFieldDef {
name: string
label: string
@@ -856,7 +1000,7 @@ export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackage
.where(
and(
eq(tagSubfields.tagTypeCode, tagTypeCode),
- eq(tagSubfields.projectId, projectId)
+ eq(tagSubfields.projectId, projectId)
)
)
.orderBy(asc(tagSubfields.sortOrder));
@@ -866,7 +1010,7 @@ export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackage
for (const sf of rows) {
// projectId가 필요한 경우 getSubfieldType과 getSubfieldOptions 함수에도 전달
const subfieldType = await getSubfieldType(sf.attributesId, projectId);
-
+
const subfieldOptions = subfieldType === "select"
? await getSubfieldOptions(sf.attributesId, projectId)
: [];
@@ -889,11 +1033,11 @@ export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackage
}
-async function getSubfieldType(attributesId: string, projectId:number): Promise<"select" | "text"> {
+async function getSubfieldType(attributesId: string, projectId: number): Promise<"select" | "text"> {
const optRows = await db
.select()
.from(tagSubfieldOptions)
- .where(and(eq(tagSubfieldOptions.attributesId, attributesId),eq(tagSubfieldOptions.projectId,projectId)))
+ .where(and(eq(tagSubfieldOptions.attributesId, attributesId), eq(tagSubfieldOptions.projectId, projectId)))
return optRows.length > 0 ? "select" : "text"
}
@@ -917,7 +1061,7 @@ export interface SubfieldOption {
/**
* SubField의 옵션 목록을 가져오는 보조 함수
*/
-async function getSubfieldOptions(attributesId: string, projectId:number): Promise<SubfieldOption[]> {
+async function getSubfieldOptions(attributesId: string, projectId: number): Promise<SubfieldOption[]> {
try {
const rows = await db
.select({
@@ -927,8 +1071,8 @@ async function getSubfieldOptions(attributesId: string, projectId:number): Promi
.from(tagSubfieldOptions)
.where(
and(
- eq(tagSubfieldOptions.attributesId, attributesId),
- eq(tagSubfieldOptions.projectId, projectId),
+ eq(tagSubfieldOptions.attributesId, attributesId),
+ eq(tagSubfieldOptions.projectId, projectId),
)
)
@@ -989,4 +1133,31 @@ export async function getTagTypes(): Promise<{ options: TagTypeOption[] }> {
export interface TagTypeOption {
id: string; // tagTypes.code 값
label: string; // tagTypes.description 값
+}
+
+export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> {
+ try {
+ // First get the contractId from contractItems
+ const contractItem = await db.query.contractItems.findFirst({
+ where: eq(contractItems.id, contractItemId),
+ columns: {
+ contractId: true
+ }
+ });
+
+ if (!contractItem) return null;
+
+ // Then get the projectId from contracts
+ const contract = await db.query.contracts.findFirst({
+ where: eq(contracts.id, contractItem.contractId),
+ columns: {
+ projectId: true
+ }
+ });
+
+ return contract?.projectId || null;
+ } catch (error) {
+ console.error("Error fetching projectId:", error);
+ return null;
+ }
} \ No newline at end of file
diff --git a/lib/tags/table/add-tag-dialog.tsx b/lib/tags/table/add-tag-dialog.tsx
index 8efb6b02..73df5aef 100644
--- a/lib/tags/table/add-tag-dialog.tsx
+++ b/lib/tags/table/add-tag-dialog.tsx
@@ -116,23 +116,25 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) {
// ---------------
// Load Class Options
// ---------------
- React.useEffect(() => {
- const loadClassOptions = async () => {
- setIsLoadingClasses(true)
- try {
- const result = await getClassOptions()
- setClassOptions(result)
- } catch (err) {
- toast.error("클래스 옵션을 불러오는데 실패했습니다.")
- } finally {
- setIsLoadingClasses(false)
- }
+// In the AddTagDialog component
+React.useEffect(() => {
+ const loadClassOptions = async () => {
+ setIsLoadingClasses(true)
+ try {
+ // Pass selectedPackageId to the function
+ const result = await getClassOptions(selectedPackageId)
+ setClassOptions(result)
+ } catch (err) {
+ toast.error("클래스 옵션을 불러오는데 실패했습니다.")
+ } finally {
+ setIsLoadingClasses(false)
}
+ }
- if (open) {
- loadClassOptions()
- }
- }, [open])
+ if (open) {
+ loadClassOptions()
+ }
+}, [open, selectedPackageId]) // Add selectedPackageId to the dependency array
// ---------------
// react-hook-form with fieldArray support for multiple rows
diff --git a/lib/tags/table/tag-table.tsx b/lib/tags/table/tag-table.tsx
index 5c8c048f..62f0a7c5 100644
--- a/lib/tags/table/tag-table.tsx
+++ b/lib/tags/table/tag-table.tsx
@@ -31,9 +31,6 @@ export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
// 1) 데이터를 가져옴 (server component -> use(...) pattern)
const [{ data, pageCount }] = React.use(promises)
-
-
-
const [rowAction, setRowAction] = React.useState<DataTableRowAction<Tag> | null>(null)
const columns = React.useMemo(
@@ -87,7 +84,7 @@ export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
enablePinning: true,
enableAdvancedFilter: true,
initialState: {
- sorting: [{ id: "createdAt", desc: true }],
+ // sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => String(originalRow.id),
@@ -97,16 +94,29 @@ export function TagsTable({ promises, selectedPackageId }: TagsTableProps) {
})
+ const [isCompact, setIsCompact] = React.useState<boolean>(false)
+
+
+ const handleCompactChange = React.useCallback((compact: boolean) => {
+ setIsCompact(compact)
+ }, [])
+
+
return (
<>
<DataTable
table={table}
+ compact={isCompact}
+
floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>}
>
<DataTableAdvancedToolbar
table={table}
filterFields={advancedFilterFields}
shallow={false}
+ enableCompactToggle={true}
+ compactStorageKey="tagTableCompact"
+ onCompactChange={handleCompactChange}
>
{/*
4) ToolbarActions에 tableData, setTableData 넘겨서
diff --git a/lib/tags/table/tags-export.tsx b/lib/tags/table/tags-export.tsx
index 4afbac6c..fa85148d 100644
--- a/lib/tags/table/tags-export.tsx
+++ b/lib/tags/table/tags-export.tsx
@@ -15,6 +15,7 @@ import { getClassOptions } from "../service"
*/
export async function exportTagsToExcel(
table: Table<Tag>,
+ selectedPackageId: number,
{
filename = "Tags",
excludeColumns = ["select", "actions", "createdAt", "updatedAt"],
@@ -26,6 +27,8 @@ export async function exportTagsToExcel(
} = {}
) {
try {
+
+
// 1. 테이블에서 컬럼 정보 가져오기
const allTableColumns = table.getAllLeafColumns()
@@ -39,7 +42,7 @@ export async function exportTagsToExcel(
const worksheet = workbook.addWorksheet("Tags")
// 3. Tag Class 옵션 가져오기
- const classOptions = await getClassOptions()
+ const classOptions = await getClassOptions(selectedPackageId)
// 4. 유효성 검사 시트 생성
const validationSheet = workbook.addWorksheet("ValidationData")
diff --git a/lib/tags/table/tags-table-toolbar-actions.tsx b/lib/tags/table/tags-table-toolbar-actions.tsx
index 497b2278..c6d13247 100644
--- a/lib/tags/table/tags-table-toolbar-actions.tsx
+++ b/lib/tags/table/tags-table-toolbar-actions.tsx
@@ -7,13 +7,14 @@ import ExcelJS from "exceljs"
import { saveAs } from "file-saver"
import { Button } from "@/components/ui/button"
-import { Download, Upload, Loader2 } from "lucide-react"
+import { Download, Upload, Loader2, RefreshCcw } from "lucide-react"
import { Tag, TagSubfields } from "@/db/schema/vendorData"
import { exportTagsToExcel } from "./tags-export"
import { AddTagDialog } from "./add-tag-dialog"
import { fetchTagSubfieldOptions, getTagNumberingRules, } from "@/lib/tag-numbering/service"
-import { bulkCreateTags, getClassOptions, getSubfieldsByTagType } from "../service"
+import { bulkCreateTags, getClassOptions, getProjectIdFromContractItemId, getSubfieldsByTagType } from "../service"
import { DeleteTagsDialog } from "./delete-tags-dialog"
+import { useRouter } from "next/navigation" // Add this import
// 태그 번호 검증을 위한 인터페이스
interface TagNumberingRule {
@@ -68,10 +69,16 @@ export function TagsTableToolbarActions({
selectedPackageId,
tableData,
}: TagsTableToolbarActionsProps) {
+ const router = useRouter() // Add this line
+
const [isPending, setIsPending] = React.useState(false)
const [isExporting, setIsExporting] = React.useState(false)
const fileInputRef = React.useRef<HTMLInputElement>(null)
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [syncId, setSyncId] = React.useState<string | null>(null)
+ const pollingRef = React.useRef<NodeJS.Timeout | null>(null)
+
// 태그 타입별 넘버링 룰 캐시
const [tagNumberingRules, setTagNumberingRules] = React.useState<Record<string, TagNumberingRule[]>>({})
const [tagOptionsCache, setTagOptionsCache] = React.useState<Record<string, TagOption[]>>({})
@@ -84,7 +91,7 @@ export function TagsTableToolbarActions({
React.useEffect(() => {
const loadClassOptions = async () => {
try {
- const options = await getClassOptions()
+ const options = await getClassOptions(selectedPackageId)
setClassOptions(options)
} catch (error) {
console.error("Failed to load class options:", error)
@@ -92,7 +99,7 @@ export function TagsTableToolbarActions({
}
loadClassOptions()
- }, [])
+ }, [selectedPackageId])
// 숨겨진 <input>을 클릭
function handleImportClick() {
@@ -123,28 +130,53 @@ export function TagsTableToolbarActions({
}
}, [tagNumberingRules])
+ const [projectId, setProjectId] = React.useState<number | null>(null);
+
+ // Add useEffect to fetch projectId when selectedPackageId changes
+ React.useEffect(() => {
+ const fetchProjectId = async () => {
+ if (selectedPackageId) {
+ try {
+ const pid = await getProjectIdFromContractItemId(selectedPackageId);
+ setProjectId(pid);
+ } catch (error) {
+ console.error("Failed to fetch project ID:", error);
+ toast.error("Failed to load project data");
+ }
+ }
+ };
+
+ fetchProjectId();
+ }, [selectedPackageId]);
+
// 특정 attributesId에 대한 옵션 가져오기
const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => {
- // 이미 캐시에 있으면 캐시된 값 사용
+ // Cache check remains the same
if (tagOptionsCache[attributesId]) {
- return tagOptionsCache[attributesId]
+ return tagOptionsCache[attributesId];
}
-
+
try {
- const options = await fetchTagSubfieldOptions(attributesId)
-
- // 캐시에 저장
+ // Only pass projectId if it's not null
+ let options: TagOption[];
+ if (projectId !== null) {
+ options = await fetchTagSubfieldOptions(attributesId, projectId);
+ } else {
+ options = []
+ }
+
+ // Update cache
setTagOptionsCache(prev => ({
...prev,
[attributesId]: options
- }))
-
- return options
+ }));
+
+ return options;
} catch (error) {
- console.error(`Error fetching options for ${attributesId}:`, error)
- return []
+ console.error(`Error fetching options for ${attributesId}:`, error);
+ return [];
}
- }, [tagOptionsCache])
+ }, [tagOptionsCache, projectId]);
// 클래스 라벨로 태그 타입 코드 찾기
const getTagTypeCodeByClassLabel = React.useCallback((classLabel: string): string | null => {
@@ -527,7 +559,7 @@ export function TagsTableToolbarActions({
setIsExporting(true)
// 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출
- await exportTagsToExcel(table, {
+ await exportTagsToExcel(table,selectedPackageId, {
filename: `Tags_${selectedPackageId}`,
excludeColumns: ["select", "actions", "createdAt", "updatedAt"],
})
@@ -541,6 +573,105 @@ export function TagsTableToolbarActions({
}
}
+ const startGetTags = async () => {
+ try {
+ setIsLoading(true)
+
+ // API 엔드포인트 호출 - 작업 시작만 요청
+ const response = await fetch('/api/cron/tags/start', {
+ method: 'POST',
+ body: JSON.stringify({ packageId: selectedPackageId })
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json()
+ throw new Error(errorData.error || 'Failed to start tag import')
+ }
+
+ const data = await response.json()
+
+ // 작업 ID 저장
+ 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'
+ )
+ setIsLoading(false)
+ }
+ }
+
+ const startPolling = (id: string) => {
+ // 이전 폴링이 있다면 제거
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current)
+ }
+
+ // 5초마다 상태 확인
+ pollingRef.current = setInterval(async () => {
+ try {
+ const response = await fetch(`/api/cron/tags/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()
+
+ // 상태 초기화
+ setIsLoading(false)
+ setSyncId(null)
+
+ // 성공 메시지 표시
+ toast.success(
+ `Tags imported successfully! ${data.result?.processedCount || 0} items processed.`
+ )
+
+ // 테이블 데이터 업데이트
+ table.resetRowSelection()
+ } else if (data.status === 'failed') {
+ // 에러 처리
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current)
+ pollingRef.current = null
+ }
+
+ setIsLoading(false)
+ setSyncId(null)
+ toast.error(data.error || 'Import failed')
+ } else if (data.status === 'processing') {
+ // 진행 상태 업데이트 (선택적)
+ if (data.progress) {
+ toast.info(`Import in progress: ${data.progress}%`, {
+ id: `import-progress-${id}`,
+ })
+ }
+ }
+ } catch (error) {
+ console.error('Error checking importing status:', error)
+ }
+ }, 5000) // 5초마다 체크
+ }
+
return (
<div className="flex items-center gap-2">
@@ -553,7 +684,18 @@ export function TagsTableToolbarActions({
selectedPackageId={selectedPackageId}
/>
) : null}
-
+ <Button
+ variant="samsung"
+ size="sm"
+ className="gap-2"
+ onClick={startGetTags}
+ disabled={isLoading}
+ >
+ <RefreshCcw className={`size-4 ${isLoading ? 'animate-spin' : ''}`} aria-hidden="true" />
+ <span className="hidden sm:inline">
+ {isLoading ? 'Syncing...' : 'Get Tags'}
+ </span>
+ </Button>
<AddTagDialog selectedPackageId={selectedPackageId} />
diff --git a/lib/tags/table/update-tag-sheet.tsx b/lib/tags/table/update-tag-sheet.tsx
index 7d213fc3..613abaa9 100644
--- a/lib/tags/table/update-tag-sheet.tsx
+++ b/lib/tags/table/update-tag-sheet.tsx
@@ -102,7 +102,6 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh
const fieldIdsRef = React.useRef<Record<string, string>>({})
const classOptionIdsRef = React.useRef<Record<string, string>>({})
- console.log(tag)
// Load class options when sheet opens
React.useEffect(() => {
@@ -111,7 +110,7 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh
setIsLoadingClasses(true)
try {
- const result = await getClassOptions()
+ const result = await getClassOptions(selectedPackageId)
setClassOptions(result)
} catch (err) {
toast.error("클래스 옵션을 불러오는데 실패했습니다.")