diff options
Diffstat (limited to 'lib/tags')
| -rw-r--r-- | lib/tags/service.ts | 181 | ||||
| -rw-r--r-- | lib/tags/table/add-tag-dialog.tsx | 199 |
2 files changed, 275 insertions, 105 deletions
diff --git a/lib/tags/service.ts b/lib/tags/service.ts index e65ab65b..a1dff137 100644 --- a/lib/tags/service.ts +++ b/lib/tags/service.ts @@ -12,6 +12,8 @@ import { countTags, insertTag, selectTags } from "./repository"; import { getErrorMessage } from "../handle-error"; import { getFormMappingsByTagType } from './form-mapping-service'; import { contractItems, contracts } from "@/db/schema/contract"; +import { getCodeListsByID } from "../sedp/sync-object-class"; +import { projects } from "@/db/schema"; // 폼 결과를 위한 인터페이스 정의 @@ -1261,52 +1263,69 @@ export interface ClassOption { * Class 옵션 목록을 가져오는 함수 * 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함 */ -export async function getClassOptions(packageId?: number) { - if (!packageId) { - throw new Error("패키지 ID가 필요합니다"); - } +export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> { + 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); - // 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 === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } - if (!packageInfo.length) { - throw new Error("패키지를 찾을 수 없거나 연결된 프로젝트가 없습니다"); - } + const projectId = packageInfo[0].projectId; - const projectId = packageInfo[0].projectId; + // 2. 태그 클래스들을 서브클래스 정보와 함께 조회 + const tagClassesWithSubclasses = await db + .select({ + id: tagClasses.id, + code: tagClasses.code, + label: tagClasses.label, + tagTypeCode: tagClasses.tagTypeCode, + subclasses: tagClasses.subclasses, + subclassRemark: tagClasses.subclassRemark, + }) + .from(tagClasses) + .where(eq(tagClasses.projectId, projectId)) + .orderBy(tagClasses.code); + // 3. 태그 타입 정보도 함께 조회 (description을 위해) + const tagTypesMap = new Map(); + const tagTypesList = await db + .select({ + code: tagTypes.code, + description: tagTypes.description, + }) + .from(tagTypes) + .where(eq(tagTypes.projectId, projectId)); - // Now get the tag classes filtered by projectId - const rows = await db - .select({ - id: tagClasses.id, - code: tagClasses.code, - label: tagClasses.label, - tagTypeCode: tagClasses.tagTypeCode, - tagTypeDescription: tagTypes.description, - }) - .from(tagClasses) - .leftJoin(tagTypes, and( - eq(tagTypes.code, tagClasses.tagTypeCode), - eq(tagTypes.projectId, tagClasses.projectId) - )) - .where(eq(tagClasses.projectId, projectId)); - - console.log(rows) - - return rows.map((row) => ({ - code: row.code, - label: row.label, - tagTypeCode: row.tagTypeCode, - tagTypeDescription: row.tagTypeDescription ?? "", - })); + tagTypesList.forEach(tagType => { + tagTypesMap.set(tagType.code, tagType.description); + }); + + // 4. 클래스 옵션으로 변환 + const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(cls => ({ + value: cls.code, + label: cls.label, + code: cls.code, + description: cls.label, + tagTypeCode: cls.tagTypeCode, + tagTypeDescription: tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, + subclasses: cls.subclasses || [], + subclassRemark: cls.subclassRemark || {}, + })); + + return classOptions; + } catch (error) { + console.error("Error fetching class options with subclasses:", error); + throw new Error("Failed to fetch class options"); + } } interface SubFieldDef { name: string @@ -1317,7 +1336,12 @@ interface SubFieldDef { delimiter: string | null } -export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackageId: number) { +export async function getSubfieldsByTagType( + tagTypeCode: string, + selectedPackageId: number, + subclassRemark: string = "", + subclass: string = "", +) { try { // 1. 먼저 contractItems에서 projectId 조회 const packageInfo = await db @@ -1352,9 +1376,10 @@ export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackage for (const sf of rows) { // projectId가 필요한 경우 getSubfieldType과 getSubfieldOptions 함수에도 전달 const subfieldType = await getSubfieldType(sf.attributesId, projectId); + const subclassMatched =subclassRemark.includes(sf.attributesId ) ? subclass: null const subfieldOptions = subfieldType === "select" - ? await getSubfieldOptions(sf.attributesId, projectId) + ? await getSubfieldOptions(sf.attributesId, projectId, subclassMatched) // subclassRemark 파라미터 추가 : []; formattedSubFields.push({ @@ -1375,6 +1400,7 @@ export async function getSubfieldsByTagType(tagTypeCode: string, selectedPackage } + async function getSubfieldType(attributesId: string, projectId: number): Promise<"select" | "text"> { const optRows = await db .select() @@ -1403,9 +1429,58 @@ export interface SubfieldOption { /** * SubField의 옵션 목록을 가져오는 보조 함수 */ -async function getSubfieldOptions(attributesId: string, projectId: number): Promise<SubfieldOption[]> { +async function getSubfieldOptions( + attributesId: string, + projectId: number, + subclass: string = "" +): Promise<SubfieldOption[]> { try { - const rows = await db + // 1. subclassRemark가 있는 경우 API에서 코드 리스트 가져와서 필터링 + if (subclass && subclass.trim() !== "") { + // 프로젝트 코드를 projectId로부터 조회 + const projectInfo = await db + .select({ + code: projects.code + }) + .from(projects) + .where(eq(projects.id, projectId)) + .limit(1); + + if (projectInfo.length === 0) { + throw new Error(`Project with ID ${projectId} not found`); + } + + const projectCode = projectInfo[0].code; + + // API에서 코드 리스트 가져오기 + const codeListValues = await getCodeListsByID(projectCode); + + // 서브클래스 리마크 값들을 분리 (쉼표, 공백 등으로 구분) + const remarkValues = subclass + .split(/[,\s]+/) // 쉼표나 공백으로 분리 + .map(val => val.trim()) + .filter(val => val.length > 0); + + if (remarkValues.length > 0) { + // REMARK 필드가 remarkValues 중 하나를 포함하고 있는 항목들 필터링 + const filteredCodeValues = codeListValues.filter(codeValue => + remarkValues.some(remarkValue => + // 대소문자 구분 없이 포함 여부 확인 + codeValue.VALUE.toLowerCase().includes(remarkValue.toLowerCase()) || + remarkValue.toLowerCase().includes(codeValue.VALUE.toLowerCase()) + ) + ); + + // 필터링된 결과를 PRNT_VALUE -> value, DESC -> label로 변환 + return filteredCodeValues.map((codeValue) => ({ + value: codeValue.PRNT_VALUE, + label: codeValue.DESC + })); + } + } + + // 2. subclassRemark가 없는 경우 기존 방식으로 DB에서 조회 + const allOptions = await db .select({ code: tagSubfieldOptions.code, label: tagSubfieldOptions.label @@ -1416,18 +1491,24 @@ async function getSubfieldOptions(attributesId: string, projectId: number): Prom eq(tagSubfieldOptions.attributesId, attributesId), eq(tagSubfieldOptions.projectId, projectId), ) - ) + ); - return rows.map((row) => ({ + return allOptions.map((row) => ({ value: row.code, label: row.label - })) + })); } catch (error) { - console.error(`Error fetching options for attribute ${attributesId}:`, error) - return [] + console.error(`Error fetching filtered options for attribute ${attributesId}:`, error); + return []; } } +export interface UpdatedClassOption extends ClassOption { + tagTypeCode: string + tagTypeDescription?: string + subclasses: {id: string, desc: string}[] + subclassRemark: Record<string, string> +} /** * Tag Type 목록을 가져오는 함수 diff --git a/lib/tags/table/add-tag-dialog.tsx b/lib/tags/table/add-tag-dialog.tsx index cb71896c..10167c62 100644 --- a/lib/tags/table/add-tag-dialog.tsx +++ b/lib/tags/table/add-tag-dialog.tsx @@ -62,10 +62,11 @@ import { TagTypeOption, } from "@/lib/tags/service" -// Updated to support multiple rows +// Updated to support multiple rows and subclass interface MultiTagFormValues { class: string; tagType: string; + subclass: string; // 새로 추가된 서브클래스 필드 rows: Array<{ [key: string]: string; tagNo: string; @@ -83,10 +84,15 @@ interface SubFieldDef { delimiter?: string } -// 클래스 옵션 인터페이스 +// 업데이트된 클래스 옵션 인터페이스 (서브클래스 정보 포함) interface UpdatedClassOption extends ClassOption { tagTypeCode: string tagTypeDescription?: string + subclasses: { + id: string; + desc: string; + }[] // 서브클래스 배열 추가 + subclassRemark: Record<string, string> // 서브클래스 리마크 추가 } interface AddTagDialogProps { @@ -99,6 +105,8 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const [open, setOpen] = React.useState(false) const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([]) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null) + const [selectedClassOption, setSelectedClassOption] = React.useState<UpdatedClassOption | null>(null) + const [selectedSubclass, setSelectedSubclass] = React.useState<string>("") const [subFields, setSubFields] = React.useState<SubFieldDef[]>([]) const [classOptions, setClassOptions] = React.useState<UpdatedClassOption[]>([]) const [classSearchTerm, setClassSearchTerm] = React.useState("") @@ -112,31 +120,29 @@ export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const fieldIdsRef = React.useRef<Record<string, string>>({}) const classOptionIdsRef = React.useRef<Record<string, string>>({}) - console.log(selectedPackageId, "tag") // --------------- - // Load Class Options + // Load Class Options (서브클래스 정보 포함) // --------------- -// 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) + React.useEffect(() => { + const loadClassOptions = async () => { + setIsLoadingClasses(true) + try { + // getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정 + const result = await getClassOptions(selectedPackageId) + setClassOptions(result) + } catch (err) { + toast.error("클래스 옵션을 불러오는데 실패했습니다.") + } finally { + setIsLoadingClasses(false) + } } - } - if (open) { - loadClassOptions() - } -}, [open, selectedPackageId]) // Add selectedPackageId to the dependency array + if (open) { + loadClassOptions() + } + }, [open, selectedPackageId]) // --------------- // react-hook-form with fieldArray support for multiple rows @@ -145,6 +151,7 @@ React.useEffect(() => { defaultValues: { tagType: "", class: "", + subclass: "", // 서브클래스 필드 추가 rows: [{ tagNo: "", description: "" @@ -158,12 +165,13 @@ React.useEffect(() => { }) // --------------- - // Load subfields by TagType code + // 서브클래스별로 필터링된 서브필드 로드 // --------------- - async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { + async function loadFilteredSubFieldsByTagTypeCode(tagTypeCode: string, subclassRemark: string, subclass: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) + // 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark ,subclass ) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -202,6 +210,10 @@ React.useEffect(() => { // --------------- async function handleSelectClass(classOption: UpdatedClassOption) { form.setValue("class", classOption.label) + form.setValue("subclass", "") // 서브클래스 초기화 + setSelectedClassOption(classOption) + setSelectedSubclass("") + if (classOption.tagTypeCode) { setSelectedTagTypeCode(classOption.tagTypeCode) // If you have tagTypeList, you can find the label @@ -211,49 +223,76 @@ React.useEffect(() => { } else if (classOption.tagTypeDescription) { form.setValue("tagType", classOption.tagTypeDescription) } - await loadSubFieldsByTagTypeCode(classOption.tagTypeCode) + + // 서브클래스가 있으면 서브필드 로딩을 하지 않고 대기 + if (classOption.subclasses && classOption.subclasses.length > 0) { + setSubFields([]) // 서브클래스 선택을 기다림 + } else { + // 서브클래스가 없으면 바로 서브필드 로딩 + await loadFilteredSubFieldsByTagTypeCode(classOption.tagTypeCode, "","") + } } } // --------------- + // Handle subclass selection + // --------------- + async function handleSelectSubclass(subclassCode: string) { + if (!selectedClassOption || !selectedTagTypeCode) return + + setSelectedSubclass(subclassCode) + form.setValue("subclass", subclassCode) + + // 선택된 서브클래스의 리마크 값 가져오기 + const subclassRemarkValue = selectedClassOption.subclassRemark[subclassCode] || "" + + // 리마크 값으로 필터링된 서브필드 로드 + await loadFilteredSubFieldsByTagTypeCode(selectedTagTypeCode, subclassRemarkValue, subclassCode) + } + + // --------------- // Build TagNo from subfields automatically for each row // --------------- React.useEffect(() => { if (subFields.length === 0) { return; } - + const subscription = form.watch((value) => { if (!value.rows || subFields.length === 0) { return; } - + const rows = [...value.rows]; rows.forEach((row, rowIndex) => { if (!row) return; - + let combined = ""; subFields.forEach((sf, idx) => { const fieldValue = row[sf.name] || ""; - combined += fieldValue; - if (fieldValue && idx < subFields.length - 1 && sf.delimiter) { + + // delimiter를 앞에 붙이기 (첫 번째 필드가 아니고, 현재 필드에 값이 있고, delimiter가 있는 경우) + if (idx > 0 && fieldValue && sf.delimiter) { combined += sf.delimiter; } + + combined += fieldValue; }); - + const currentTagNo = form.getValues(`rows.${rowIndex}.tagNo`); if (currentTagNo !== combined) { form.setValue(`rows.${rowIndex}.tagNo`, combined, { - shouldDirty: true, // Changed from false to true - shouldTouch: true, // Changed from false to true - shouldValidate: true, // Changed from false to true + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, }); } }); }); - + return () => subscription.unsubscribe(); }, [subFields, form]); + // --------------- // Check if tag numbers are valid // --------------- @@ -263,9 +302,10 @@ React.useEffect(() => { const tagNo = row.tagNo; return tagNo && tagNo.trim() !== "" && !tagNo.includes("??"); }); - }, [form.watch()]); // Watch the entire form to catch all changes + }, [form.watch()]); + // --------------- - // Submit handler for multiple tags + // Submit handler for multiple tags (서브클래스 정보 포함) // --------------- async function onSubmit(data: MultiTagFormValues) { if (!selectedPackageId) { @@ -280,10 +320,11 @@ React.useEffect(() => { // Process each row for (const row of data.rows) { - // Create tag data from the row and shared class/tagType + // Create tag data from the row and shared class/tagType/subclass const tagData: CreateTagSchema = { tagType: data.tagType, class: data.class, + // subclass: data.subclass, // 서브클래스 정보 추가 tagNo: row.tagNo, description: row.description, ...Object.fromEntries( @@ -316,7 +357,6 @@ React.useEffect(() => { if (failedTags.length > 0) { console.log("Failed tags:", failedTags); - toast.error(`${failedTags.length}개의 태그 생성에 실패했습니다.`); } @@ -339,11 +379,10 @@ React.useEffect(() => { // Add a new row // --------------- function addRow() { - // Create a properly typed row with index signature to allow dynamic properties const newRow: { tagNo: string; description: string; - [key: string]: string; // This allows any string key with string values + [key: string]: string; } = { tagNo: "", description: "" @@ -355,8 +394,6 @@ React.useEffect(() => { }); append(newRow); - - // Force form validation after row is added setTimeout(() => form.trigger(), 0); } @@ -365,18 +402,14 @@ React.useEffect(() => { // --------------- function duplicateRow(index: number) { const rowToDuplicate = form.getValues(`rows.${index}`); - // Use proper typing with index signature const newRow: { tagNo: string; description: string; [key: string]: string; } = { ...rowToDuplicate }; - // Clear the tagNo field as it will be auto-generated newRow.tagNo = ""; append(newRow); - - // Force form validation after row is duplicated setTimeout(() => form.trigger(), 0); } @@ -400,7 +433,7 @@ React.useEffect(() => { ) return ( - <FormItem className="w-1/2"> + <FormItem className="w-1/3"> <FormLabel>Class</FormLabel> <FormControl> <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> @@ -482,6 +515,45 @@ React.useEffect(() => { } // --------------- + // Render Subclass field (새로 추가) + // --------------- + function renderSubclassField(field: any) { + const hasSubclasses = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 + + if (!hasSubclasses) { + return null + } + + return ( + <FormItem className="w-1/3"> + <FormLabel>Subclass</FormLabel> + <FormControl> + <Select + value={field.value || ""} + onValueChange={(value) => { + field.onChange(value) + handleSelectSubclass(value) + }} + disabled={!selectedClassOption} + > + <SelectTrigger className="h-9"> + <SelectValue placeholder="서브클래스 선택..." /> + </SelectTrigger> + <SelectContent> + {selectedClassOption?.subclasses.map((subclass) => ( + <SelectItem key={subclass.id} value={subclass.id}> + {subclass.desc} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + ) + } + + // --------------- // Render TagType field (readonly after class selection) // --------------- function renderTagTypeField(field: any) { @@ -494,8 +566,10 @@ React.useEffect(() => { [isReadOnly] ) + const width = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 ? "w-1/3" : "w-2/3" + return ( - <FormItem className="w-1/2"> + <FormItem className={width}> <FormLabel>Tag Type</FormLabel> <FormControl> {isReadOnly ? ( @@ -536,9 +610,13 @@ React.useEffect(() => { } if (subFields.length === 0 && selectedTagTypeCode) { + const message = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 + ? "서브클래스를 선택해주세요." + : "이 태그 유형에 대한 필드가 없습니다." + return ( <div className="py-4 text-center text-muted-foreground"> - 이 태그 유형에 대한 필드가 없습니다. + {message} </div> ) } @@ -713,9 +791,7 @@ React.useEffect(() => { /> )} </FormControl> - {/* <FormMessage>{sf.expression}</FormMessage> */} </FormItem> - )} /> </TableCell> @@ -779,7 +855,7 @@ React.useEffect(() => { variant="outline" className="w-full border-dashed" onClick={addRow} - disabled={!selectedTagTypeCode || isLoadingSubFields} + disabled={!selectedTagTypeCode || isLoadingSubFields || subFields.length === 0} > <Plus className="h-4 w-4 mr-2" /> 새 행 추가 @@ -808,9 +884,12 @@ React.useEffect(() => { form.reset({ tagType: "", class: "", + subclass: "", rows: [{ tagNo: "", description: "" }] }); setSelectedTagTypeCode(null); + setSelectedClassOption(null); + setSelectedSubclass(""); setSubFields([]); } setOpen(o); @@ -826,7 +905,7 @@ React.useEffect(() => { <DialogHeader> <DialogTitle>새 태그 추가</DialogTitle> <DialogDescription> - 클래스를 선택하여 태그 유형과 하위 필드를 로드한 다음, 여러 행을 추가하여 여러 태그를 생성하세요. + 클래스와 서브클래스를 선택하여 태그 유형과 하위 필드를 로드한 다음, 여러 행을 추가하여 여러 태그를 생성하세요. </DialogDescription> </DialogHeader> @@ -835,7 +914,7 @@ React.useEffect(() => { onSubmit={form.handleSubmit(onSubmit)} className="space-y-6" > - {/* 클래스 및 태그 유형 선택 */} + {/* 클래스, 서브클래스, 태그 유형 선택 */} <div className="flex gap-4"> <FormField key="class-field" @@ -845,6 +924,13 @@ React.useEffect(() => { /> <FormField + key="subclass-field" + control={form.control} + name="subclass" + render={({ field }) => renderSubclassField(field)} + /> + + <FormField key="tag-type-field" control={form.control} name="tagType" @@ -865,11 +951,14 @@ React.useEffect(() => { form.reset({ tagType: "", class: "", + subclass: "", rows: [{ tagNo: "", description: "" }] }); setOpen(false); setSubFields([]); setSelectedTagTypeCode(null); + setSelectedClassOption(null); + setSelectedSubclass(""); }} disabled={isSubmitting} > |
