diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
| commit | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch) | |
| tree | f84750fa6cac954d5e31221fc47a54c655fc06a9 /lib/tags | |
| parent | 230ce796836c25df26c130dbcd616ef97d12b2ec (diff) | |
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/tags')
| -rw-r--r-- | lib/tags/form-mapping-service.ts | 3 | ||||
| -rw-r--r-- | lib/tags/service.ts | 229 | ||||
| -rw-r--r-- | lib/tags/table/add-tag-dialog copy.tsx | 637 | ||||
| -rw-r--r-- | lib/tags/table/add-tag-dialog.tsx | 4 | ||||
| -rw-r--r-- | lib/tags/table/tags-table-toolbar-actions.tsx | 2 | ||||
| -rw-r--r-- | lib/tags/table/update-tag-sheet.tsx | 2 |
6 files changed, 198 insertions, 679 deletions
diff --git a/lib/tags/form-mapping-service.ts b/lib/tags/form-mapping-service.ts index 4b772ab6..19b3ab14 100644 --- a/lib/tags/form-mapping-service.ts +++ b/lib/tags/form-mapping-service.ts @@ -17,6 +17,7 @@ export interface FormMapping { */ export async function getFormMappingsByTagType( tagType: string, + projectId: number, classCode?: string ): Promise<FormMapping[]> { @@ -32,6 +33,7 @@ export async function getFormMappingsByTagType( .from(tagTypeClassFormMappings) .where(and( eq(tagTypeClassFormMappings.tagTypeLabel, tagType), + eq(tagTypeClassFormMappings.projectId, projectId), eq(tagTypeClassFormMappings.classLabel, classCode) )) @@ -51,6 +53,7 @@ export async function getFormMappingsByTagType( .from(tagTypeClassFormMappings) .where(and( eq(tagTypeClassFormMappings.tagTypeLabel, tagType), + eq(tagTypeClassFormMappings.projectId, projectId), eq(tagTypeClassFormMappings.classLabel, "DEFAULT") )) diff --git a/lib/tags/service.ts b/lib/tags/service.ts index 034c106f..8477b1fb 100644 --- a/lib/tags/service.ts +++ b/lib/tags/service.ts @@ -8,10 +8,10 @@ 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, deleteTagById, deleteTagsByIds, insertTag, selectTags } from "./repository"; +import { countTags, insertTag, selectTags } from "./repository"; import { getErrorMessage } from "../handle-error"; import { getFormMappingsByTagType } from './form-mapping-service'; -import { contractItems } from "@/db/schema/contract"; +import { contractItems, contracts } from "@/db/schema/contract"; // 폼 결과를 위한 인터페이스 정의 @@ -110,16 +110,21 @@ export async function createTag( return await db.transaction(async (tx) => { // 1) 선택된 contractItem의 contractId 가져오기 const contractItemResult = await tx - .select({ contractId: contractItems.contractId }) - .from(contractItems) - .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" } } const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인 const duplicateCheck = await tx @@ -142,6 +147,7 @@ export async function createTag( // 3) 태그 타입에 따른 폼 정보 가져오기 const formMappings = await getFormMappingsByTagType( validated.data.tagType, + projectId, // projectId 전달 validated.data.class ) @@ -149,9 +155,12 @@ export async function createTag( if (!formMappings || formMappings.length === 0) { console.log( "No form mappings found for tag type:", - validated.data.tagType + validated.data.tagType, + "in project:", + projectId ) } + // 4) 이 태그 타입에 대한 주요 폼(첫 번째 폼)을 찾거나 생성 let primaryFormId: number | null = null @@ -283,16 +292,21 @@ export async function updateTag( // 2) 선택된 contractItem의 contractId 가져오기 const contractItemResult = await tx - .select({ contractId: contractItems.contractId }) - .from(contractItems) - .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" } } const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인 if (originalTag.tagNo !== validated.data.tagNo) { @@ -327,6 +341,7 @@ export async function updateTag( // 4-1) 태그 타입에 따른 폼 정보 가져오기 const formMappings = await getFormMappingsByTagType( validated.data.tagType, + projectId, // projectId 전달 validated.data.class ) @@ -334,7 +349,9 @@ export async function updateTag( if (!formMappings || formMappings.length === 0) { console.log( "No form mappings found for tag type:", - validated.data.tagType + validated.data.tagType, + "in project:", + projectId ) } @@ -450,10 +467,14 @@ export async function bulkCreateTags( try { // 단일 트랜잭션으로 모든 작업 처리 return await db.transaction(async (tx) => { - // 1. 컨트랙트 ID 조회 (한 번만) + // 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만) const contractItemResult = await tx - .select({ contractId: contractItems.contractId }) + .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); @@ -462,6 +483,7 @@ export async function bulkCreateTags( } const contractId = contractItemResult[0].contractId; + const projectId = contractItemResult[0].projectId; // projectId 추출 // 2. 모든 태그 번호 중복 검사 (한 번에) const tagNos = tagsfromExcel.map(tag => tag.tagNo); @@ -482,25 +504,111 @@ export async function bulkCreateTags( // 3. 태그별 폼 정보 처리 및 태그 생성 const createdTags = []; + const allFormsInfo = []; // 모든 태그에 대한 폼 정보 저장 + + // 태그 유형별 폼 매핑 캐싱 (성능 최적화) + const formMappingsCache = new Map(); for (const tagData of tagsfromExcel) { - // 각 태그 유형에 대한 폼 처리 (createTag 함수와 유사한 로직) - const formMappings = await getFormMappingsByTagType(tagData.tagType, tagData.class); - let primaryFormId = null; + // 캐시 키 생성 (tagType + class) + const cacheKey = `${tagData.tagType}|${tagData.class || 'NONE'}`; + + // 폼 매핑 가져오기 (캐시 사용) + let formMappings; + if (formMappingsCache.has(cacheKey)) { + formMappings = formMappingsCache.get(cacheKey); + } else { + // 각 태그 유형에 대한 폼 매핑 조회 (projectId 전달) + formMappings = await getFormMappingsByTagType( + tagData.tagType, + projectId, // projectId 전달 + tagData.class + ); + formMappingsCache.set(cacheKey, formMappings); + } + + // 폼 처리 로직 + let primaryFormId: number | null = null; + const createdOrExistingForms: CreatedOrExistingForm[] = []; - // 폼 처리 로직 (생략...) + if (formMappings && formMappings.length > 0) { + for (const formMapping of formMappings) { + // 해당 폼이 이미 존재하는지 확인 + const existingForm = await tx + .select({ id: forms.id }) + .from(forms) + .where( + and( + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) + ) + ) + .limit(1); + + let formId: number; + if (existingForm.length > 0) { + // 이미 존재하면 해당 ID 사용 + formId = existingForm[0].id; + createdOrExistingForms.push({ + id: formId, + formCode: formMapping.formCode, + formName: formMapping.formName, + isNewlyCreated: false, + }); + } else { + // 존재하지 않으면 새로 생성 + const insertResult = await tx + .insert(forms) + .values({ + contractItemId: selectedPackageId, + formCode: formMapping.formCode, + formName: formMapping.formName, + }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }); + + formId = insertResult[0].id; + createdOrExistingForms.push({ + id: formId, + formCode: insertResult[0].formCode, + formName: insertResult[0].formName, + isNewlyCreated: true, + }); + } + + // 첫 번째 폼을 "주요 폼"으로 설정하여 태그 생성 시 사용 + if (primaryFormId === null) { + primaryFormId = formId; + } + } + } else { + console.log( + "No form mappings found for tag type:", + tagData.tagType, + "class:", + tagData.class || "NONE", + "in project:", + projectId + ); + } // 태그 생성 const [newTag] = await insertTag(tx, { contractItemId: selectedPackageId, formId: primaryFormId, tagNo: tagData.tagNo, - class: tagData.class, + class: tagData.class || "", tagType: tagData.tagType, description: tagData.description || null, }); createdTags.push(newTag); + + // 해당 태그의 폼 정보 저장 + allFormsInfo.push({ + tagNo: tagData.tagNo, + forms: createdOrExistingForms, + primaryFormId, + }); } // 4. 캐시 무효화 (한 번만) @@ -512,17 +620,17 @@ export async function bulkCreateTags( success: true, data: { createdCount: createdTags.length, - tags: createdTags + tags: createdTags, + formsInfo: allFormsInfo } }; }); } catch (err: any) { console.error("bulkCreateTags error:", err); - return { error: err.message || "Failed to create tags" }; + return { error: getErrorMessage(err) || "Failed to create tags" }; } } - /** 복수 삭제 */ interface RemoveTagsInput { ids: number[]; @@ -548,6 +656,22 @@ export async function removeTags(input: RemoveTagsInput) { 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; + // 1) 삭제 대상 tag들을 미리 조회 const tagsToDelete = await tx .select({ @@ -583,7 +707,7 @@ export async function removeTags(input: RemoveTagsInput) { ) // 3-2) 이 태그 타입/클래스에 연결된 폼 매핑 가져오기 - const formMappings = await getFormMappingsByTagType(tagType, classValue); + const formMappings = await getFormMappingsByTagType(tagType,projectId,classValue); if (!formMappings.length) continue; @@ -707,21 +831,45 @@ interface SubFieldDef { delimiter: string | null } -export async function getSubfieldsByTagType(tagTypeCode: string) { +export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackageId: number) { try { + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } + + const projectId = packageInfo[0].projectId; + + // 2. 올바른 projectId를 사용하여 tagSubfields 조회 const rows = await db .select() .from(tagSubfields) - .where(eq(tagSubfields.tagTypeCode, tagTypeCode)) - .orderBy(asc(tagSubfields.sortOrder)) + .where( + and( + eq(tagSubfields.tagTypeCode, tagTypeCode), + eq(tagSubfields.projectId, projectId) + ) + ) + .orderBy(asc(tagSubfields.sortOrder)); // 각 row -> SubFieldDef - const formattedSubFields: SubFieldDef[] = [] + const formattedSubFields: SubFieldDef[] = []; for (const sf of rows) { - const subfieldType = await getSubfieldType(sf.attributesId) + // projectId가 필요한 경우 getSubfieldType과 getSubfieldOptions 함수에도 전달 + const subfieldType = await getSubfieldType(sf.attributesId, projectId); + const subfieldOptions = subfieldType === "select" - ? await getSubfieldOptions(sf.attributesId) - : [] + ? await getSubfieldOptions(sf.attributesId, projectId) + : []; formattedSubFields.push({ name: sf.attributesId.toLowerCase(), @@ -730,22 +878,22 @@ export async function getSubfieldsByTagType(tagTypeCode: string) { options: subfieldOptions, expression: sf.expression, delimiter: sf.delimiter, - }) + }); } - return { subFields: formattedSubFields } + return { subFields: formattedSubFields }; } catch (error) { - console.error("Error fetching subfields by tag type:", error) - throw new Error("Failed to fetch subfields") + console.error("Error fetching subfields by tag type:", error); + throw new Error("Failed to fetch subfields"); } } -async function getSubfieldType(attributesId: string): Promise<"select" | "text"> { +async function getSubfieldType(attributesId: string, projectId:number): Promise<"select" | "text"> { const optRows = await db .select() .from(tagSubfieldOptions) - .where(eq(tagSubfieldOptions.attributesId, attributesId)) + .where(and(eq(tagSubfieldOptions.attributesId, attributesId),eq(tagSubfieldOptions.projectId,projectId))) return optRows.length > 0 ? "select" : "text" } @@ -769,7 +917,7 @@ export interface SubfieldOption { /** * SubField의 옵션 목록을 가져오는 보조 함수 */ -async function getSubfieldOptions(attributesId: string): Promise<SubfieldOption[]> { +async function getSubfieldOptions(attributesId: string, projectId:number): Promise<SubfieldOption[]> { try { const rows = await db .select({ @@ -777,7 +925,12 @@ async function getSubfieldOptions(attributesId: string): Promise<SubfieldOption[ label: tagSubfieldOptions.label }) .from(tagSubfieldOptions) - .where(eq(tagSubfieldOptions.attributesId, attributesId)) + .where( + and( + eq(tagSubfieldOptions.attributesId, attributesId), + eq(tagSubfieldOptions.projectId, projectId), + ) + ) return rows.map((row) => ({ value: row.code, diff --git a/lib/tags/table/add-tag-dialog copy.tsx b/lib/tags/table/add-tag-dialog copy.tsx deleted file mode 100644 index e9f84933..00000000 --- a/lib/tags/table/add-tag-dialog copy.tsx +++ /dev/null @@ -1,637 +0,0 @@ -"use client" - -import * as React from "react" -import { useRouter } from "next/navigation" // <-- 1) Import router from App Router -import { useForm, useWatch } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { toast } from "sonner" -import { Loader2, ChevronsUpDown, Check } from "lucide-react" - -import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Form, - FormField, - FormItem, - FormControl, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { - Popover, - PopoverTrigger, - PopoverContent, -} from "@/components/ui/popover" -import { - Command, - CommandInput, - CommandList, - CommandGroup, - CommandItem, - CommandEmpty, -} from "@/components/ui/command" -import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" -import { cn } from "@/lib/utils" - -import type { CreateTagSchema } from "@/lib/tags/validations" -import { createTagSchema } from "@/lib/tags/validations" -import { - createTag, - getSubfieldsByTagType, - getClassOptions, - type ClassOption, - TagTypeOption, -} from "@/lib/tags/service" - -// SubFieldDef for clarity -interface SubFieldDef { - name: string - label: string - type: "select" | "text" - options?: { value: string; label: string }[] - expression?: string - delimiter?: string -} - -// 클래스 옵션 인터페이스 -interface UpdatedClassOption extends ClassOption { - tagTypeCode: string - tagTypeDescription?: string -} - -interface AddTagDialogProps { - selectedPackageId: number | null -} - -export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { - const router = useRouter() // <-- 2) Use the router hook - - const [open, setOpen] = React.useState(false) - const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([]) - const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null) - const [subFields, setSubFields] = React.useState<SubFieldDef[]>([]) - const [classOptions, setClassOptions] = React.useState<UpdatedClassOption[]>([]) - const [classSearchTerm, setClassSearchTerm] = React.useState("") - const [isLoadingClasses, setIsLoadingClasses] = React.useState(false) - const [isLoadingSubFields, setIsLoadingSubFields] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) - - // ID management - const selectIdRef = React.useRef(0) - const getUniqueSelectId = React.useCallback(() => `select-${selectIdRef.current++}`, []) - const fieldIdsRef = React.useRef<Record<string, string>>({}) - const classOptionIdsRef = React.useRef<Record<string, string>>({}) - - // --------------- - // Load Class Options - // --------------- - React.useEffect(() => { - const loadClassOptions = async () => { - setIsLoadingClasses(true) - try { - const result = await getClassOptions() - setClassOptions(result) - } catch (err) { - toast.error("Failed to load class options") - } finally { - setIsLoadingClasses(false) - } - } - - if (open) { - loadClassOptions() - } - }, [open]) - - // --------------- - // react-hook-form - // --------------- - const form = useForm<CreateTagSchema>({ - resolver: zodResolver(createTagSchema), - defaultValues: { - tagType: "", - tagNo: "", - description: "", - functionCode: "", - seqNumber: "", - valveAcronym: "", - processUnit: "", - class: "", - }, - }) - - // watch - const { tagNo, ...fieldsToWatch } = useWatch({ - control: form.control, - }) - - // --------------- - // Load subfields by TagType code - // --------------- - async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { - setIsLoadingSubFields(true) - try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode) - const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ - name: field.name, - label: field.label, - type: field.type, - options: field.options || [], - expression: field.expression ?? undefined, - delimiter: field.delimiter ?? undefined, - })) - setSubFields(formattedSubFields) - selectIdRef.current = 0 - return true - } catch (err) { - toast.error("Failed to load subfields") - setSubFields([]) - return false - } finally { - setIsLoadingSubFields(false) - } - } - - // --------------- - // Handle class selection - // --------------- - async function handleSelectClass(classOption: UpdatedClassOption) { - form.setValue("class", classOption.label) - if (classOption.tagTypeCode) { - setSelectedTagTypeCode(classOption.tagTypeCode) - // If you have tagTypeList, you can find the label - const tagType = tagTypeList.find(t => t.id === classOption.tagTypeCode) - if (tagType) { - form.setValue("tagType", tagType.label) - } else if (classOption.tagTypeDescription) { - form.setValue("tagType", classOption.tagTypeDescription) - } - await loadSubFieldsByTagTypeCode(classOption.tagTypeCode) - } - } - - // --------------- - // Render subfields - // --------------- - function renderSubFields() { - if (isLoadingSubFields) { - return ( - <div className="flex justify-center items-center py-8"> - <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> - <span className="ml-3 text-muted-foreground">Loading fields...</span> - </div> - ) - } - if (subFields.length === 0 && selectedTagTypeCode) { - return ( - <div className="py-4 text-center text-muted-foreground"> - No fields available for this tag type. - </div> - ) - } - if (subFields.length === 0) { - return null - } - - return subFields.map((sf, index) => { - if (!fieldIdsRef.current[`${sf.name}-${index}`]) { - fieldIdsRef.current[`${sf.name}-${index}`] = - `field-${sf.name}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` - } - const fieldId = fieldIdsRef.current[`${sf.name}-${index}`] - const selectId = getUniqueSelectId() - - return ( - <FormField - key={fieldId} - control={form.control} - name={sf.name as keyof CreateTagSchema} - render={({ field }) => ( - <FormItem> - <FormLabel>{sf.label}</FormLabel> - <FormControl> - {sf.type === "select" ? ( - <Select - value={field.value || ""} - onValueChange={field.onChange} - > - <SelectTrigger className="w-full"> - <SelectValue - placeholder={`Select ${sf.label}`} - className={ - !field.value ? "text-muted-foreground text-opacity-60" : "" - } - /> - </SelectTrigger> - <SelectContent - align="start" - side="bottom" - style={{ width: 400, maxWidth: 400 }} - sideOffset={4} - id={selectId} - > - {sf.options?.map((opt, optIndex) => { - const optionKey = `${fieldId}-option-${opt.value}-${optIndex}` - return ( - <SelectItem - key={optionKey} - value={opt.value} - className="multi-line-select-item pr-6" - title={opt.label} - > - {opt.label} - </SelectItem> - ) - })} - </SelectContent> - </Select> - ) : ( - <Input - placeholder={`Enter ${sf.label}`} - {...field} - className={ - !field.value - ? "placeholder:text-muted-foreground placeholder:text-opacity-60" - : "" - } - /> - )} - </FormControl> - <FormMessage> - {sf.expression && ( - <span - className="text-xs text-muted-foreground truncate block" - title={sf.expression} - > - 형식: {sf.expression} - </span> - )} - </FormMessage> - </FormItem> - )} - /> - ) - }) - } - - // --------------- - // Build TagNo from subfields automatically - // --------------- - React.useEffect(() => { - if (subFields.length === 0) { - form.setValue("tagNo", "", { shouldDirty: false }) - } - - const subscription = form.watch((value, { name }) => { - if (!name || name === "tagNo" || subFields.length === 0) { - return - } - let combined = "" - subFields.forEach((sf, idx) => { - const fieldValue = form.getValues(sf.name as keyof CreateTagSchema) || "" - combined += fieldValue - if (fieldValue && idx < subFields.length - 1 && sf.delimiter) { - combined += sf.delimiter - } - }) - const currentTagNo = form.getValues("tagNo") - if (currentTagNo !== combined) { - form.setValue("tagNo", combined, { - shouldDirty: false, - shouldTouch: false, - shouldValidate: false, - }) - } - }) - - return () => subscription.unsubscribe() - }, [subFields, form]) - - // --------------- - // Basic validation for TagNo - // --------------- - const isTagNoValid = React.useMemo(() => { - const val = form.getValues("tagNo") - return val && val.trim() !== "" && !val.includes("??") - }, [fieldsToWatch]) - - // --------------- - // Submit handler - // --------------- - async function onSubmit(data: CreateTagSchema) { - if (!selectedPackageId) { - toast.error("No selectedPackageId.") - return - } - setIsSubmitting(true) - try { - const res = await createTag(data, selectedPackageId) - if ("error" in res) { - toast.error(`Error: ${res.error}`) - return - } - - toast.success("Tag created successfully!") - - // 3) Refresh or navigate after creation: - // Option A: If you just want to refresh the same route: - router.refresh() - - // Option B: If you want to go to /partners/vendor-data/tag/{selectedPackageId} - // router.push(`/partners/vendor-data/tag/${selectedPackageId}?r=${Date.now()}`) - - // (If you want to reset the form dialog or close it, do that too) - form.reset() - setOpen(false) - } catch (err) { - toast.error("Failed to create tag.") - } finally { - setIsSubmitting(false) - } - } - - // --------------- - // Render Class field - // --------------- - function renderClassField(field: any) { - const [popoverOpen, setPopoverOpen] = React.useState(false) - - const buttonId = React.useMemo( - () => `class-button-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, - [] - ) - const popoverContentId = React.useMemo( - () => `class-popover-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, - [] - ) - const commandId = React.useMemo( - () => `class-command-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, - [] - ) - - return ( - <FormItem> - <FormLabel>Class</FormLabel> - <FormControl> - <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> - <PopoverTrigger asChild> - <Button - key={buttonId} - type="button" - variant="outline" - className="w-full justify-between" - disabled={isLoadingClasses} - > - {isLoadingClasses ? ( - <> - <span>Loading classes...</span> - <Loader2 className="ml-2 h-4 w-4 animate-spin" /> - </> - ) : ( - <> - <span className="truncate"> - {field.value || "Select Class..."} - </span> - <ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" /> - </> - )} - </Button> - </PopoverTrigger> - <PopoverContent key={popoverContentId} className="w-full p-0"> - <Command key={commandId}> - <CommandInput - key={`${commandId}-input`} - placeholder="Search Class..." - value={classSearchTerm} - onValueChange={setClassSearchTerm} - /> - <CommandList key={`${commandId}-list`}> - <CommandEmpty key={`${commandId}-empty`}>No class found.</CommandEmpty> - <CommandGroup key={`${commandId}-group`}> - {classOptions.map((opt) => { - if (!classOptionIdsRef.current[opt.code]) { - classOptionIdsRef.current[opt.code] = - `class-${opt.code}-${Date.now()}-${Math.random() - .toString(36) - .slice(2, 9)}` - } - const optionId = classOptionIdsRef.current[opt.code] - - return ( - <CommandItem - key={optionId} - onSelect={() => { - field.onChange(opt.label) - setPopoverOpen(false) - handleSelectClass(opt) - }} - value={opt.label} - className="truncate" - title={opt.label} - > - <span className="truncate">{opt.label}</span> - <Check - key={`${optionId}-check`} - className={cn( - "ml-auto h-4 w-4 flex-shrink-0", - field.value === opt.label ? "opacity-100" : "opacity-0" - )} - /> - </CommandItem> - ) - })} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> - </FormControl> - <FormMessage /> - </FormItem> - ) - } - - // --------------- - // Render TagType field (readonly after class selection) - // --------------- - function renderTagTypeField(field: any) { - const isReadOnly = !!selectedTagTypeCode - const inputId = React.useMemo( - () => - `tag-type-input-${isReadOnly ? "readonly" : "editable"}-${Date.now()}-${Math.random() - .toString(36) - .slice(2, 9)}`, - [isReadOnly] - ) - - return ( - <FormItem> - <FormLabel>Tag Type</FormLabel> - <FormControl> - {isReadOnly ? ( - <Input - key={`tag-type-readonly-${inputId}`} - {...field} - readOnly - className="bg-muted" - /> - ) : ( - <Input - key={`tag-type-placeholder-${inputId}`} - {...field} - readOnly - placeholder="Tag Type is determined by selected Class" - className="bg-muted" - /> - )} - </FormControl> - <FormMessage /> - </FormItem> - ) - } - - // --------------- - // Reset IDs/states when dialog closes - // --------------- - React.useEffect(() => { - if (!open) { - fieldIdsRef.current = {} - classOptionIdsRef.current = {} - selectIdRef.current = 0 - } - }, [open]) - - return ( - <Dialog - open={open} - onOpenChange={(o) => { - if (!o) { - form.reset() - setSelectedTagTypeCode(null) - setSubFields([]) - } - setOpen(o) - }} - > - <DialogTrigger asChild> - <Button variant="default" size="sm"> - Add Tag - </Button> - </DialogTrigger> - - <DialogContent className="max-h-[80vh] flex flex-col"> - <DialogHeader> - <DialogTitle>Add New Tag</DialogTitle> - <DialogDescription> - Choose a Class, and the Tag Type and subfields will be automatically loaded. - </DialogDescription> - </DialogHeader> - - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="max-h-[70vh] flex flex-col" - > - <div className="flex-1 overflow-auto px-4 space-y-4"> - {/* Class */} - <FormField - key="class-field" - control={form.control} - name="class" - render={({ field }) => renderClassField(field)} - /> - - {/* TagType (read-only) */} - <FormField - key="tag-type-field" - control={form.control} - name="tagType" - render={({ field }) => renderTagTypeField(field)} - /> - - {/* SubFields */} - <div className="flex-1 overflow-auto px-2 py-2 space-y-4 max-h-[300px]"> - {renderSubFields()} - </div> - - {/* TagNo (read-only) */} - <FormField - key="tag-no-field" - control={form.control} - name="tagNo" - render={({ field }) => ( - <FormItem> - <FormLabel>Tag No</FormLabel> - <FormControl> - <Input - {...field} - readOnly - className="bg-muted truncate" - title={field.value || ""} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* Description */} - <FormField - key="description-field" - control={form.control} - name="description" - render={({ field }) => ( - <FormItem> - <FormLabel>Description</FormLabel> - <FormControl> - <Input - {...field} - placeholder="Enter description..." - className="truncate" - title={field.value || ""} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - {/* Footer */} - <DialogFooter className="bg-background z-10 pt-4 px-4 py-4"> - <Button - type="button" - variant="outline" - onClick={() => { - form.reset() - setOpen(false) - setSubFields([]) - setSelectedTagTypeCode(null) - }} - disabled={isSubmitting || isLoadingSubFields} - > - Cancel - </Button> - <Button - type="submit" - disabled={isSubmitting || isLoadingSubFields || !isTagNoValid} - > - {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - Create - </Button> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ) -}
\ 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 e1e176cf..8efb6b02 100644 --- a/lib/tags/table/add-tag-dialog.tsx +++ b/lib/tags/table/add-tag-dialog.tsx @@ -90,7 +90,7 @@ interface UpdatedClassOption extends ClassOption { } interface AddTagDialogProps { - selectedPackageId: number | null + selectedPackageId: number } export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { @@ -159,7 +159,7 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, diff --git a/lib/tags/table/tags-table-toolbar-actions.tsx b/lib/tags/table/tags-table-toolbar-actions.tsx index 8d53d3f3..497b2278 100644 --- a/lib/tags/table/tags-table-toolbar-actions.tsx +++ b/lib/tags/table/tags-table-toolbar-actions.tsx @@ -160,7 +160,7 @@ export function TagsTableToolbarActions({ } try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode) + const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) // API 응답을 SubFieldDef 형식으로 변환 const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ diff --git a/lib/tags/table/update-tag-sheet.tsx b/lib/tags/table/update-tag-sheet.tsx index 27a1bdcb..7d213fc3 100644 --- a/lib/tags/table/update-tag-sheet.tsx +++ b/lib/tags/table/update-tag-sheet.tsx @@ -165,7 +165,7 @@ export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSh async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, |
