diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-17 18:04:10 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-17 18:04:10 +0900 |
| commit | 7433eea5b4bbc0899e255b88e1a7e91f26e9d95b (patch) | |
| tree | cad02c119fd41545e24a98734488962c78ed895d /lib | |
| parent | 6c11fccc84f4c84fa72ee01f9caad9f76f35cea2 (diff) | |
(김준회) 자재그룹 선택기 오류수정, 공용사용을 위한 다이얼로그 컴포넌트 구현, data-table 오류 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/material-groups/services.ts | 67 | ||||
| -rw-r--r-- | lib/material-groups/table/material-group-table-columns.tsx | 23 | ||||
| -rw-r--r-- | lib/material-groups/table/material-group-table.tsx | 16 | ||||
| -rw-r--r-- | lib/material-groups/validations.ts | 4 | ||||
| -rw-r--r-- | lib/material/material-group-service.ts | 26 | ||||
| -rw-r--r-- | lib/material/vendor-material/add-confirmed-material.tsx | 123 |
6 files changed, 111 insertions, 148 deletions
diff --git a/lib/material-groups/services.ts b/lib/material-groups/services.ts index be683077..3ee9886d 100644 --- a/lib/material-groups/services.ts +++ b/lib/material-groups/services.ts @@ -3,10 +3,10 @@ import { and, asc, desc, ilike, or, sql, eq } from 'drizzle-orm'; import db from '@/db/db'; import { filterColumns } from "@/lib/filter-columns"; -import { materialSearchView } from "@/db/schema/items"; +import { MATERIAL_GROUP_MASTER } from "@/db/schema/MDG/mdg"; -// 자재그룹 뷰의 컬럼 타입 정의 -type MaterialGroupColumn = keyof typeof materialSearchView.$inferSelect; +// 자재그룹 테이블의 컬럼 타입 정의 +type MaterialGroupColumn = keyof typeof MATERIAL_GROUP_MASTER.$inferSelect; export interface GetMaterialGroupsInput { page: number; @@ -16,13 +16,16 @@ export interface GetMaterialGroupsInput { id: MaterialGroupColumn; desc: boolean; }>; - filters?: any[]; + filters?: Array<{ + id: string; + value: unknown; + }>; joinOperator: 'and' | 'or'; } /** - * 자재그룹 목록을 조회합니다. - * materialSearchView를 사용하여 MATKL(자재그룹코드)와 ATWTB(자재그룹 설명)의 고유한 조합을 조회 + * 자재그룹 목록을 조회 + * 자재그룹코드/자재그룹명/UOM 조회 */ export async function getMaterialGroups(input: GetMaterialGroupsInput) { const safePerPage = Math.min(input.perPage, 100); @@ -32,8 +35,8 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { // 고급 필터링 const advancedWhere = filterColumns({ - table: materialSearchView, - filters: (input.filters || []) as any, + table: MATERIAL_GROUP_MASTER, + filters: input.filters || [], joinOperator: input.joinOperator, }); @@ -42,8 +45,9 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { if (input.search) { const s = `%${input.search}%`; globalWhere = or( - ilike(materialSearchView.materialGroupCode, s), // 자재그룹코드 - ilike(materialSearchView.materialGroupDesc, s), // 자재그룹명 + ilike(MATERIAL_GROUP_MASTER.materialGroupCode, s), // 자재그룹코드 + ilike(MATERIAL_GROUP_MASTER.materialGroupDescription, s), // 자재그룹명 + ilike(MATERIAL_GROUP_MASTER.materialGroupUom, s), // UOM ); } @@ -52,19 +56,20 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { // 정렬 처리 - 타입 안전하게 처리 const orderBy = input.sort.length > 0 ? input.sort.map((item) => { - const column = materialSearchView[item.id]; + const column = MATERIAL_GROUP_MASTER[item.id]; return item.desc ? desc(column) : asc(column); }) - : [asc(materialSearchView.materialGroupCode)]; + : [asc(MATERIAL_GROUP_MASTER.materialGroupCode)]; // 데이터 조회 const { data, total } = await db.transaction(async (tx) => { const data = await tx .select({ - materialGroupCode: materialSearchView.materialGroupCode, - materialGroupDesc: materialSearchView.materialGroupDesc, + materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode, + materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription, + materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom, }) - .from(materialSearchView) + .from(MATERIAL_GROUP_MASTER) .where(finalWhere) .orderBy(...orderBy) .offset(offset) @@ -74,7 +79,7 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { .select({ count: sql<number>`count(*)` }) - .from(materialSearchView) + .from(MATERIAL_GROUP_MASTER) .where(finalWhere); const total = Number(totalResult[0]?.count) || 0; @@ -100,8 +105,8 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite try { // 고급 필터링 const advancedWhere = filterColumns({ - table: materialSearchView, - filters: (input.filters || []) as any, + table: MATERIAL_GROUP_MASTER, + filters: input.filters || [], joinOperator: input.joinOperator || "and", }); @@ -110,8 +115,9 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite if (input.search) { const s = `%${input.search}%`; globalWhere = or( - ilike(materialSearchView.materialGroupCode, s), - ilike(materialSearchView.materialGroupDesc, s), + ilike(MATERIAL_GROUP_MASTER.materialGroupCode, s), + ilike(MATERIAL_GROUP_MASTER.materialGroupDescription, s), + ilike(MATERIAL_GROUP_MASTER.materialGroupUom, s), ); } @@ -120,18 +126,19 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite // 정렬 처리 - 타입 안전하게 처리 const orderBy = input.sort.length > 0 ? input.sort.map((item) => { - const column = materialSearchView[item.id]; + const column = MATERIAL_GROUP_MASTER[item.id]; return item.desc ? desc(column) : asc(column); }) - : [asc(materialSearchView.materialGroupCode)]; + : [asc(MATERIAL_GROUP_MASTER.materialGroupCode)]; // 전체 데이터 조회 (클라이언트에서 가상화 처리) const data = await db .select({ - materialGroupCode: materialSearchView.materialGroupCode, - materialGroupDesc: materialSearchView.materialGroupDesc, + materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode, + materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription, + materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom, }) - .from(materialSearchView) + .from(MATERIAL_GROUP_MASTER) .where(finalWhere) .orderBy(...orderBy); @@ -148,9 +155,13 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite export async function getMaterialGroupDetail(materialGroupCode: string) { try { const materialGroup = await db - .select() - .from(materialSearchView) - .where(eq(materialSearchView.materialGroupCode, materialGroupCode)) + .select({ + materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode, + materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription, + materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom, + }) + .from(MATERIAL_GROUP_MASTER) + .where(eq(MATERIAL_GROUP_MASTER.materialGroupCode, materialGroupCode)) .limit(1); return materialGroup.length > 0 ? materialGroup[0] : null; diff --git a/lib/material-groups/table/material-group-table-columns.tsx b/lib/material-groups/table/material-group-table-columns.tsx index 08e730e3..f41ed887 100644 --- a/lib/material-groups/table/material-group-table-columns.tsx +++ b/lib/material-groups/table/material-group-table-columns.tsx @@ -8,7 +8,8 @@ import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table- // MaterialGroup 타입 정의 (서비스에서 반환되는 타입과 일치) type MaterialGroup = { materialGroupCode: string | null; - materialGroupDesc: string | null; + materialGroupDescription: string | null; + materialGroupUom?: string | null; } /** @@ -36,12 +37,12 @@ export function getColumns(): ColumnDef<MaterialGroup>[] { enableHiding: false, }, { - accessorKey: "materialGroupDesc", + accessorKey: "materialGroupDescription", header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="자재그룹명" /> ), cell: ({ row }) => { - const value = row.getValue("materialGroupDesc") as string | null + const value = row.getValue("materialGroupDescription") as string | null return ( <div className="max-w-[400px] truncate"> {value || "-"} @@ -51,6 +52,22 @@ export function getColumns(): ColumnDef<MaterialGroup>[] { enableSorting: true, enableHiding: false, }, + { + accessorKey: "materialGroupUom", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="UOM" /> + ), + cell: ({ row }) => { + const value = row.getValue("materialGroupUom") as string | null + return ( + <div className="max-w-[80px] truncate"> + {value || "-"} + </div> + ) + }, + enableSorting: true, + enableHiding: false, + }, ] // ---------------------------------------------------------------- diff --git a/lib/material-groups/table/material-group-table.tsx b/lib/material-groups/table/material-group-table.tsx index 766b5054..a5617e9b 100644 --- a/lib/material-groups/table/material-group-table.tsx +++ b/lib/material-groups/table/material-group-table.tsx @@ -20,7 +20,8 @@ import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" // MaterialGroup 타입 정의 (서비스에서 반환되는 타입과 일치) type MaterialGroup = { materialGroupCode: string | null; - materialGroupDesc: string | null; + materialGroupDescription: string | null; + materialGroupUom?: string | null; } interface MaterialGroupTableProps { @@ -50,9 +51,13 @@ export function MaterialGroupTable({ promises }: MaterialGroupTableProps) { label: "자재그룹코드", }, { - id: "materialGroupDesc", + id: "materialGroupDescription", label: "자재그룹명", }, + { + id: "materialGroupUom", + label: "UOM", + }, ] const advancedFilterFields: DataTableAdvancedFilterField<MaterialGroup>[] = [ @@ -62,10 +67,15 @@ export function MaterialGroupTable({ promises }: MaterialGroupTableProps) { type: "text", }, { - id: "materialGroupDesc", + id: "materialGroupDescription", label: "자재그룹명", type: "text", }, + { + id: "materialGroupUom", + label: "UOM", + type: "text", + }, ] // 확장된 useDataTable 훅 사용 (pageSize 기반 자동 전환) diff --git a/lib/material-groups/validations.ts b/lib/material-groups/validations.ts index c379c833..f2acfaee 100644 --- a/lib/material-groups/validations.ts +++ b/lib/material-groups/validations.ts @@ -8,7 +8,7 @@ import { import * as z from "zod" import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" -import { materialSearchView } from "@/db/schema/items" +import { MATERIAL_GROUP_MASTER } from "@/db/schema/MDG/mdg" // MaterialGroup 검색 파라미터 캐시 정의 export const searchParamsCache = createSearchParamsCache({ @@ -17,7 +17,7 @@ export const searchParamsCache = createSearchParamsCache({ perPage: parseAsInteger.withDefault(10), // 확장된 타입으로 정렬 파서 사용 - sort: getSortingStateParser<typeof materialSearchView>().withDefault([ + sort: getSortingStateParser<typeof MATERIAL_GROUP_MASTER>().withDefault([ { id: "materialGroupCode", desc: false }, ]), diff --git a/lib/material/material-group-service.ts b/lib/material/material-group-service.ts index ae0ca725..bf58d243 100644 --- a/lib/material/material-group-service.ts +++ b/lib/material/material-group-service.ts @@ -2,11 +2,12 @@ import { sql, SQL } from "drizzle-orm"; import db from "@/db/db"; -import { materialSearchView } from "@/db/schema/items"; +import { MATERIAL_GROUP_MASTER } from "@/db/schema/MDG/mdg"; export interface MaterialSearchItem { materialGroupCode: string; - materialGroupDesc: string; + materialGroupDescription: string; + materialGroupUom?: string; displayText: string; // 애플리케이션 레벨에서 계산된 필드 } @@ -39,25 +40,29 @@ export async function searchMaterialsForSelector( if (query.trim()) { const searchPattern = `%${query.trim()}%`; searchWhere = sql`( - ${materialSearchView.materialGroupCode} ILIKE ${searchPattern} OR - ${materialSearchView.materialGroupDesc} ILIKE ${searchPattern} + ${MATERIAL_GROUP_MASTER.materialGroupCode} ILIKE ${searchPattern} OR + ${MATERIAL_GROUP_MASTER.materialGroupDescription} ILIKE ${searchPattern} )`; } const { data, total } = await db.transaction(async (tx) => { // 데이터 조회 const data = await tx - .select() - .from(materialSearchView) + .select({ + materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode, + materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription, + materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom, + }) + .from(MATERIAL_GROUP_MASTER) .where(searchWhere) - .orderBy(materialSearchView.materialGroupCode, materialSearchView.materialGroupDesc) + .orderBy(MATERIAL_GROUP_MASTER.materialGroupCode, MATERIAL_GROUP_MASTER.materialGroupDescription) .limit(perPage) .offset(offset); // 총 개수 조회 const countResult = await tx .select({ count: sql<number>`count(*)` }) - .from(materialSearchView) + .from(MATERIAL_GROUP_MASTER) .where(searchWhere); const total = countResult[0]?.count || 0; @@ -65,8 +70,9 @@ export async function searchMaterialsForSelector( return { data: data.map((row) => ({ materialGroupCode: row.materialGroupCode, - materialGroupDesc: row.materialGroupDesc, - displayText: `${row.materialGroupCode || ''} - ${row.materialGroupDesc || ''}`, // 애플리케이션 레벨에서 생성 + materialGroupDescription: row.materialGroupDescription, + materialGroupUom: row.materialGroupUom, + displayText: `${row.materialGroupCode || ''} - ${row.materialGroupDescription || ''}`, // 애플리케이션 레벨에서 생성 })), total, }; diff --git a/lib/material/vendor-material/add-confirmed-material.tsx b/lib/material/vendor-material/add-confirmed-material.tsx index 5e9415f4..78940c00 100644 --- a/lib/material/vendor-material/add-confirmed-material.tsx +++ b/lib/material/vendor-material/add-confirmed-material.tsx @@ -3,20 +3,8 @@ import * as React from "react"; import { useState } from "react"; import { useSession } from "next-auth/react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; import { toast } from "sonner"; -import { Plus, Loader2 } from "lucide-react"; -import { MaterialGroupSelector } from "@/components/common/material/material-group-selector"; +import { MaterialGroupSelectorDialogMulti } from "@/components/common/material/material-group-selector-dialog-multi"; import { MaterialSearchItem } from "@/lib/material/material-group-service"; import { addConfirmedMaterial, VendorPossibleMaterial } from "../vendor-possible-material-service"; @@ -32,43 +20,22 @@ export function AddConfirmedMaterial({ onMaterialAdded, }: AddConfirmedMaterialProps) { const { data: session } = useSession(); - const [open, setOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedMaterials, setSelectedMaterials] = useState<MaterialSearchItem[]>([]); // 이미 등록된 자재그룹코드들의 Set const existingMaterialCodes = new Set( - existingConfirmedMaterials.map(material => material.itemCode).filter(Boolean) + existingConfirmedMaterials.map(material => material.itemCode).filter((code): code is string => Boolean(code)) ); - // 자재 선택 시 중복 체크 - const handleMaterialsChange = (materials: MaterialSearchItem[]) => { - // 이미 등록된 자재가 있는지 확인 - const duplicatedMaterials = materials.filter(material => - existingMaterialCodes.has(material.materialGroupCode) - ); - - if (duplicatedMaterials.length > 0) { - const duplicatedCodes = duplicatedMaterials.map(m => m.materialGroupCode).join(', '); - toast.error(`이미 등록된 자재그룹코드입니다: ${duplicatedCodes}`); - - // 중복되지 않은 자재만 선택 - const validMaterials = materials.filter(material => - !existingMaterialCodes.has(material.materialGroupCode) - ); - setSelectedMaterials(validMaterials); - } else { - setSelectedMaterials(materials); - } - }; - - const handleSubmit = async () => { + // 자재 선택 완료 시 처리 + const handleMaterialsSelect = async (materials: MaterialSearchItem[]) => { if (!session?.user) { toast.error("로그인이 필요합니다."); return; } - if (selectedMaterials.length === 0) { + if (materials.length === 0) { toast.error("추가할 자재를 선택해주세요."); return; } @@ -77,10 +44,10 @@ export function AddConfirmedMaterial({ try { // 선택된 자재들을 각각 추가 - for (const material of selectedMaterials) { + for (const material of materials) { const materialData = { itemCode: material.materialGroupCode, - itemName: material.materialName, + itemName: material.materialGroupDescription, }; await addConfirmedMaterial( @@ -91,11 +58,10 @@ export function AddConfirmedMaterial({ ); } - toast.success(`${selectedMaterials.length}개의 자재가 확정정보에 추가되었습니다.`); + toast.success(`${materials.length}개의 자재가 확정정보에 추가되었습니다.`); // 폼 리셋 setSelectedMaterials([]); - setOpen(false); // 부모 컴포넌트에 추가 완료 알림 onMaterialAdded?.(); @@ -119,65 +85,18 @@ export function AddConfirmedMaterial({ }; return ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogTrigger asChild> - <Button size="sm" className="gap-2"> - <Plus className="h-4 w-4" /> - 추가 등록 - </Button> - </DialogTrigger> - <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>확정정보 자재 추가</DialogTitle> - <DialogDescription> - 구매담당자 권한으로 확정 공급품목을 추가합니다. 상세 정보는 I/F를 통해 업데이트됩니다. - </DialogDescription> - </DialogHeader> - - <div className="space-y-4"> - {/* 자재 선택 */} - <div className="space-y-2"> - <Label>자재 선택 *</Label> - <MaterialGroupSelector - selectedMaterials={selectedMaterials} - onMaterialsChange={handleMaterialsChange} - singleSelect={false} - placeholder="자재그룹코드 또는 자재명으로 검색..." - noValuePlaceHolder="자재를 선택해주세요" - maxSelections={10} - closeOnSelect={false} - excludeMaterialCodes={existingMaterialCodes} - /> - {existingMaterialCodes.size > 0 && ( - <p className="text-xs text-muted-foreground"> - 💡 이미 등록된 자재그룹코드는 자동으로 제외됩니다. - </p> - )} - <p className="text-sm text-muted-foreground"> - 최대 10개까지 선택 가능합니다. 등록자는 현재 로그인한 사용자로 자동 설정됩니다. - </p> - </div> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => setOpen(false)} - disabled={isLoading} - > - 취소 - </Button> - <Button - type="button" - onClick={handleSubmit} - disabled={isLoading || selectedMaterials.length === 0} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "추가 중..." : "추가"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> + <MaterialGroupSelectorDialogMulti + triggerLabel={isLoading ? "추가 중..." : "➕ 추가 등록"} + selectedMaterials={selectedMaterials} + onMaterialsSelect={handleMaterialsSelect} + placeholder="자재그룹코드 또는 자재명으로 검색..." + title="확정정보 자재 추가" + description="구매담당자 권한으로 확정 공급품목을 추가합니다. 상세 정보는 I/F를 통해 업데이트됩니다." + disabled={isLoading} + triggerVariant="default" + excludeMaterialCodes={existingMaterialCodes} + showInitialData={true} + showSelectedInTrigger={false} + /> ); } |
