diff options
Diffstat (limited to 'lib/tags')
| -rw-r--r-- | lib/tags/service.ts | 337 | ||||
| -rw-r--r-- | lib/tags/table/add-tag-dialog.tsx | 32 | ||||
| -rw-r--r-- | lib/tags/table/tag-table.tsx | 18 | ||||
| -rw-r--r-- | lib/tags/table/tags-export.tsx | 5 | ||||
| -rw-r--r-- | lib/tags/table/tags-table-toolbar-actions.tsx | 178 | ||||
| -rw-r--r-- | lib/tags/table/update-tag-sheet.tsx | 3 |
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("클래스 옵션을 불러오는데 실패했습니다.") |
