summaryrefslogtreecommitdiff
path: root/lib/tags
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
commit9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch)
treef84750fa6cac954d5e31221fc47a54c655fc06a9 /lib/tags
parent230ce796836c25df26c130dbcd616ef97d12b2ec (diff)
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/tags')
-rw-r--r--lib/tags/form-mapping-service.ts3
-rw-r--r--lib/tags/service.ts229
-rw-r--r--lib/tags/table/add-tag-dialog copy.tsx637
-rw-r--r--lib/tags/table/add-tag-dialog.tsx4
-rw-r--r--lib/tags/table/tags-table-toolbar-actions.tsx2
-rw-r--r--lib/tags/table/update-tag-sheet.tsx2
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,