summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-27 19:53:08 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-27 19:53:08 +0900
commit7e161e6ef81b2798aa91c8e705ec9e968675079a (patch)
treea5b461005a2dca80cb900a0d70654225b7794f57
parent2d7c41263ee4fda0285397905884e20dec5d8a1f (diff)
(김준회) 자재그룹 선택기에서 코드를 보여주지 않는 선택기 추가개발, 회원가입시 사용하도록 변경 *박성형프로요청
-rw-r--r--components/common/material/material-group-selector-dialog-multi-without-code.tsx300
-rw-r--r--components/common/material/material-group-selector-dialog-single-without-code.tsx209
-rw-r--r--components/common/material/material-group-selector.tsx30
-rw-r--r--components/signup/join-form.tsx13
-rw-r--r--lib/material/material-group-service.ts122
5 files changed, 622 insertions, 52 deletions
diff --git a/components/common/material/material-group-selector-dialog-multi-without-code.tsx b/components/common/material/material-group-selector-dialog-multi-without-code.tsx
new file mode 100644
index 00000000..67496d27
--- /dev/null
+++ b/components/common/material/material-group-selector-dialog-multi-without-code.tsx
@@ -0,0 +1,300 @@
+"use client";
+
+import React, { useState, useCallback } from "react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { MaterialGroupSelector } from "./material-group-selector";
+import { MaterialSearchItem } from "@/lib/material/material-group-service";
+import { X } from "lucide-react";
+
+/**
+ * 자재그룹 다중 선택 Dialog 컴포넌트
+ *
+ * @description
+ * - MaterialGroupSelector를 Dialog로 래핑한 다중 선택 컴포넌트
+ * - 버튼 클릭 시 Dialog가 열리고, 여러 자재를 선택한 후 확인 버튼으로 완료
+ * - 최대 선택 개수 제한 가능
+ *
+ * @MaterialSearchItem_Structure
+ * 상태에서 관리되는 자재그룹 객체의 형태:
+ * ```typescript
+ * interface MaterialSearchItem {
+ * materialGroupCode: string; // 자재그룹코드 (예: "BG2001")
+ * materialGroupDescription: string; // 자재그룹명 (예: "DOUBLE WETDOOR HINGE")
+ * materialGroupUom?: string; // 단위 (예: "EA", "KG") - 선택적
+ * displayText: string; // 표시용 텍스트 (code + " - " + description)
+ * }
+ * ```
+ *
+ * @state
+ * - open: Dialog 열림/닫힘 상태
+ * - selectedMaterials: 현재 선택된 자재들 (배열)
+ * - tempSelectedMaterials: Dialog 내에서 임시로 선택된 자재들 (확인 버튼 클릭 전까지)
+ *
+ * @callback
+ * - onMaterialsSelect: 자재 선택 완료 시 호출되는 콜백
+ * - 매개변수: MaterialSearchItem[]
+ * - 선택된 자재들의 배열 (빈 배열일 수도 있음)
+ *
+ * @usage
+ * ```tsx
+ * <MaterialGroupSelectorDialogMulti
+ * triggerLabel="자재 선택 (다중)"
+ * selectedMaterials={selectedMaterials}
+ * onMaterialsSelect={(materials) => {
+ * setSelectedMaterials(materials);
+ * console.log('선택된 자재들:', materials);
+ * }}
+ * maxSelections={5}
+ * placeholder="자재를 검색하세요..."
+ * />
+ * ```
+ */
+
+interface MaterialGroupSelectorDialogMultiProps {
+ /** Dialog를 여는 트리거 버튼 텍스트 */
+ triggerLabel?: string;
+ /** 현재 선택된 자재들 */
+ selectedMaterials?: MaterialSearchItem[];
+ /** 자재 선택 완료 시 호출되는 콜백 */
+ onMaterialsSelect?: (materials: MaterialSearchItem[]) => void;
+ /** 최대 선택 가능한 자재 개수 */
+ maxSelections?: number;
+ /** 검색 입력창 placeholder */
+ placeholder?: string;
+ /** Dialog 제목 */
+ title?: string;
+ /** Dialog 설명 */
+ description?: string;
+ /** 트리거 버튼 비활성화 여부 */
+ disabled?: boolean;
+ /** 트리거 버튼 variant */
+ triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+ /** 제외할 자재그룹코드들 */
+ excludeMaterialCodes?: Set<string>;
+ /** 초기 데이터 표시 여부 */
+ showInitialData?: boolean;
+ /** 트리거 버튼에서 선택된 자재들을 표시할지 여부 */
+ showSelectedInTrigger?: boolean;
+}
+
+export function MaterialGroupSelectorDialogMulti({
+ triggerLabel = "자재 선택",
+ selectedMaterials = [],
+ onMaterialsSelect,
+ maxSelections,
+ placeholder = "자재를 검색하세요...",
+ title = "자재 선택 (다중)",
+ description,
+ disabled = false,
+ triggerVariant = "outline",
+ excludeMaterialCodes,
+ showInitialData = true,
+ showSelectedInTrigger = true,
+}: MaterialGroupSelectorDialogMultiProps) {
+ // Dialog 열림/닫힘 상태
+ const [open, setOpen] = useState(false);
+
+ // Dialog 내에서 임시로 선택된 자재들 (확인 버튼 클릭 전까지)
+ const [tempSelectedMaterials, setTempSelectedMaterials] = useState<MaterialSearchItem[]>([]);
+
+ // Dialog 설명 동적 생성
+ const dialogDescription = description ||
+ (maxSelections
+ ? `원하는 자재를 검색하고 선택해주세요. (최대 ${maxSelections}개)`
+ : "원하는 자재를 검색하고 선택해주세요."
+ );
+
+ // Dialog 열림 시 현재 선택된 자재들로 임시 선택 초기화
+ const handleOpenChange = useCallback((newOpen: boolean) => {
+ setOpen(newOpen);
+ if (newOpen) {
+ setTempSelectedMaterials([...selectedMaterials]);
+ }
+ }, [selectedMaterials]);
+
+ // 자재 선택 처리 (Dialog 내에서)
+ const handleMaterialsChange = useCallback((materials: MaterialSearchItem[]) => {
+ setTempSelectedMaterials(materials);
+ }, []);
+
+ // 확인 버튼 클릭 시 선택 완료
+ const handleConfirm = useCallback(() => {
+ onMaterialsSelect?.(tempSelectedMaterials);
+ setOpen(false);
+ }, [tempSelectedMaterials, onMaterialsSelect]);
+
+ // 취소 버튼 클릭 시
+ const handleCancel = useCallback(() => {
+ setTempSelectedMaterials([...selectedMaterials]);
+ setOpen(false);
+ }, [selectedMaterials]);
+
+ // 전체 선택 해제
+ const handleClearAll = useCallback(() => {
+ setTempSelectedMaterials([]);
+ }, []);
+
+ // 개별 자재 제거 (트리거 버튼에서)
+ const handleRemoveMaterial = useCallback((materialToRemove: MaterialSearchItem, e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const newMaterials = selectedMaterials.filter(
+ (material) => !(material.materialGroupCode === materialToRemove.materialGroupCode &&
+ material.materialGroupDescription === materialToRemove.materialGroupDescription)
+ );
+ onMaterialsSelect?.(newMaterials);
+ }, [selectedMaterials, onMaterialsSelect]);
+
+ // 트리거 버튼 렌더링
+ const renderTriggerContent = () => {
+ if (selectedMaterials.length === 0) {
+ return triggerLabel;
+ }
+
+ if (!showSelectedInTrigger) {
+ return `${triggerLabel} (${selectedMaterials.length}개)`;
+ }
+
+ return (
+ <div className="flex flex-wrap gap-1 items-center max-w-full">
+ <span className="shrink-0">{triggerLabel}:</span>
+ {selectedMaterials.slice(0, 2).map((material) => (
+ <Badge
+ key={`${material.materialGroupCode}-${material.materialGroupDescription}`}
+ variant="secondary"
+ className="gap-1 pr-1 max-w-[120px]"
+ >
+ <span className="truncate text-xs">
+ {material.materialGroupDescription}
+ </span>
+ {!disabled && (
+ <button
+ type="button"
+ className="ml-1 h-3 w-3 rounded-sm hover:bg-red-100 flex items-center justify-center"
+ onClick={(e) => handleRemoveMaterial(material, e)}
+ >
+ <X className="h-3 w-3 hover:text-red-500" />
+ </button>
+ )}
+ </Badge>
+ ))}
+ {selectedMaterials.length > 2 && (
+ <Badge variant="outline" className="text-xs">
+ +{selectedMaterials.length - 2}개
+ </Badge>
+ )}
+ </div>
+ );
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button
+ variant={triggerVariant}
+ disabled={disabled}
+ className="min-h-[2.5rem] h-auto justify-start"
+ >
+ {renderTriggerContent()}
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{dialogDescription}</DialogDescription>
+ </DialogHeader>
+
+ <div className="py-4">
+ <MaterialGroupSelector
+ selectedMaterials={tempSelectedMaterials}
+ onMaterialsChange={handleMaterialsChange}
+ singleSelect={false}
+ maxSelections={maxSelections}
+ placeholder={placeholder}
+ noValuePlaceHolder="자재를 선택해주세요"
+ closeOnSelect={false}
+ excludeMaterialCodes={excludeMaterialCodes}
+ showInitialData={showInitialData}
+ hideCode={true}
+ />
+
+ {/* 선택된 자재 개수 표시 */}
+ <div className="mt-2 text-sm text-muted-foreground">
+ {maxSelections ? (
+ `선택됨: ${tempSelectedMaterials.length}/${maxSelections}개`
+ ) : (
+ `선택됨: ${tempSelectedMaterials.length}개`
+ )}
+ </div>
+ </div>
+
+ <DialogFooter className="gap-2">
+ <Button variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ {tempSelectedMaterials.length > 0 && (
+ <Button variant="ghost" onClick={handleClearAll}>
+ 전체 해제
+ </Button>
+ )}
+ <Button onClick={handleConfirm}>
+ 확인 ({tempSelectedMaterials.length}개)
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+/**
+ * 사용 예시:
+ *
+ * ```tsx
+ * const [selectedMaterials, setSelectedMaterials] = useState<MaterialSearchItem[]>([]);
+ *
+ * return (
+ * <MaterialGroupSelectorDialogMulti
+ * triggerLabel="자재 선택"
+ * selectedMaterials={selectedMaterials}
+ * onMaterialsSelect={(materials) => {
+ * setSelectedMaterials(materials);
+ * console.log('선택된 자재들:', materials.map(m => ({
+ * code: m.materialGroupCode,
+ * description: m.materialGroupDescription,
+ * uom: m.materialGroupUom
+ * })));
+ * }}
+ * maxSelections={5}
+ * title="필수 자재 선택"
+ * description="프로젝트에 필요한 자재들을 선택해주세요."
+ * showSelectedInTrigger={true}
+ * />
+ * );
+ * ```
+ *
+ * @advanced_usage
+ * ```tsx
+ * // 제외할 자재가 있는 경우
+ * const excludedCodes = new Set(['MAT001', 'MAT002']);
+ *
+ * <MaterialGroupSelectorDialogMulti
+ * selectedMaterials={selectedMaterials}
+ * onMaterialsSelect={setSelectedMaterials}
+ * excludeMaterialCodes={excludedCodes}
+ * maxSelections={3}
+ * triggerVariant="default"
+ * showSelectedInTrigger={false}
+ * />
+ * ```
+ */
diff --git a/components/common/material/material-group-selector-dialog-single-without-code.tsx b/components/common/material/material-group-selector-dialog-single-without-code.tsx
new file mode 100644
index 00000000..e3ef5eb8
--- /dev/null
+++ b/components/common/material/material-group-selector-dialog-single-without-code.tsx
@@ -0,0 +1,209 @@
+"use client";
+
+import React, { useState, useCallback } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { MaterialGroupSelector } from "./material-group-selector";
+import { MaterialSearchItem } from "@/lib/material/material-group-service";
+
+/**
+ * 자재그룹 단일 선택 Dialog 컴포넌트
+ *
+ * @description
+ * - MaterialGroupSelector를 Dialog로 래핑한 단일 선택 컴포넌트
+ * - 버튼 클릭 시 Dialog가 열리고, 자재를 선택하면 Dialog가 닫히며 결과를 반환
+ *
+ * @MaterialSearchItem_Structure
+ * 상태에서 관리되는 자재그룹 객체의 형태:
+ * ```typescript
+ * interface MaterialSearchItem {
+ * materialGroupCode: string; // 자재그룹코드 (예: "BG2001")
+ * materialGroupDescription: string; // 자재그룹명 (예: "DOUBLE WETDOOR HINGE")
+ * materialGroupUom?: string; // 단위 (예: "EA", "KG")
+ * displayText: string; // 표시용 텍스트 (code + " - " + description)
+ * }
+ * ```
+ *
+ * @state
+ * - open: Dialog 열림/닫힘 상태
+ * - selectedMaterial: 현재 선택된 자재 (단일)
+ * - tempSelectedMaterial: Dialog 내에서 임시로 선택된 자재 (확인 버튼 클릭 전까지)
+ *
+ * @callback
+ * - onMaterialSelect: 자재 선택 완료 시 호출되는 콜백
+ * - 매개변수: MaterialSearchItem | null
+ * - 선택된 자재 정보 또는 null (선택 해제 시)
+ *
+ * @usage
+ * ```tsx
+ * <MaterialGroupSelectorDialogSingle
+ * triggerLabel="자재 선택"
+ * selectedMaterial={selectedMaterial}
+ * onMaterialSelect={(material) => {
+ * setSelectedMaterial(material);
+ * console.log('선택된 자재:', material);
+ * }}
+ * placeholder="자재를 검색하세요..."
+ * />
+ * ```
+ */
+
+interface MaterialGroupSelectorDialogSingleProps {
+ /** Dialog를 여는 트리거 버튼 텍스트 */
+ triggerLabel?: string;
+ /** 현재 선택된 자재 */
+ selectedMaterial?: MaterialSearchItem | null;
+ /** 자재 선택 완료 시 호출되는 콜백 */
+ onMaterialSelect?: (material: MaterialSearchItem | null) => void;
+ /** 검색 입력창 placeholder */
+ placeholder?: string;
+ /** Dialog 제목 */
+ title?: string;
+ /** Dialog 설명 */
+ description?: string;
+ /** 트리거 버튼 비활성화 여부 */
+ disabled?: boolean;
+ /** 트리거 버튼 variant */
+ triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+ /** 제외할 자재그룹코드들 */
+ excludeMaterialCodes?: Set<string>;
+ /** 초기 데이터 표시 여부 */
+ showInitialData?: boolean;
+}
+
+export function MaterialGroupSelectorDialogSingle({
+ triggerLabel = "자재 선택",
+ selectedMaterial = null,
+ onMaterialSelect,
+ placeholder = "자재를 검색하세요...",
+ title = "자재 선택",
+ description = "원하는 자재를 검색하고 선택해주세요.",
+ disabled = false,
+ triggerVariant = "outline",
+ excludeMaterialCodes,
+ showInitialData = true,
+}: MaterialGroupSelectorDialogSingleProps) {
+ // Dialog 열림/닫힘 상태
+ const [open, setOpen] = useState(false);
+
+ // Dialog 내에서 임시로 선택된 자재 (확인 버튼 클릭 전까지)
+ const [tempSelectedMaterial, setTempSelectedMaterial] = useState<MaterialSearchItem | null>(null);
+
+ // Dialog 열림 시 현재 선택된 자재로 임시 선택 초기화
+ const handleOpenChange = useCallback((newOpen: boolean) => {
+ setOpen(newOpen);
+ if (newOpen) {
+ setTempSelectedMaterial(selectedMaterial || null);
+ }
+ }, [selectedMaterial]);
+
+ // 자재 선택 처리 (Dialog 내에서)
+ const handleMaterialChange = useCallback((materials: MaterialSearchItem[]) => {
+ setTempSelectedMaterial(materials.length > 0 ? materials[0] : null);
+ }, []);
+
+ // 확인 버튼 클릭 시 선택 완료
+ const handleConfirm = useCallback(() => {
+ onMaterialSelect?.(tempSelectedMaterial);
+ setOpen(false);
+ }, [tempSelectedMaterial, onMaterialSelect]);
+
+ // 취소 버튼 클릭 시
+ const handleCancel = useCallback(() => {
+ setTempSelectedMaterial(selectedMaterial || null);
+ setOpen(false);
+ }, [selectedMaterial]);
+
+ // 선택 해제
+ const handleClear = useCallback(() => {
+ setTempSelectedMaterial(null);
+ }, []);
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button variant={triggerVariant} disabled={disabled}>
+ {selectedMaterial ? (
+ <span className="truncate">
+ {selectedMaterial.displayText}
+ </span>
+ ) : (
+ triggerLabel
+ )}
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{description}</DialogDescription>
+ </DialogHeader>
+
+ <div className="py-4">
+ <MaterialGroupSelector
+ selectedMaterials={tempSelectedMaterial ? [tempSelectedMaterial] : []}
+ onMaterialsChange={handleMaterialChange}
+ singleSelect={true}
+ placeholder={placeholder}
+ noValuePlaceHolder="자재를 선택해주세요"
+ closeOnSelect={false}
+ excludeMaterialCodes={excludeMaterialCodes}
+ showInitialData={showInitialData}
+ hideCode={true}
+ />
+ </div>
+
+ <DialogFooter className="gap-2">
+ <Button variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ {tempSelectedMaterial && (
+ <Button variant="ghost" onClick={handleClear}>
+ 선택 해제
+ </Button>
+ )}
+ <Button onClick={handleConfirm}>
+ 확인
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+/**
+ * 사용 예시:
+ *
+ * ```tsx
+ * const [selectedMaterial, setSelectedMaterial] = useState<MaterialSearchItem | null>(null);
+ *
+ * return (
+ * <MaterialGroupSelectorDialogSingle
+ * triggerLabel="자재 선택"
+ * selectedMaterial={selectedMaterial}
+ * onMaterialSelect={(material) => {
+ * setSelectedMaterial(material);
+ * if (material) {
+ * console.log('선택된 자재:', {
+ * code: material.materialGroupCode,
+ * description: material.materialGroupDescription,
+ * uom: material.materialGroupUom
+ * });
+ * } else {
+ * console.log('자재 선택이 해제되었습니다.');
+ * }
+ * }}
+ * title="자재 선택"
+ * description="필요한 자재를 검색하고 선택해주세요."
+ * />
+ * );
+ * ```
+ */ \ No newline at end of file
diff --git a/components/common/material/material-group-selector.tsx b/components/common/material/material-group-selector.tsx
index f0276810..98625caa 100644
--- a/components/common/material/material-group-selector.tsx
+++ b/components/common/material/material-group-selector.tsx
@@ -33,6 +33,7 @@ interface MaterialGroupSelectorProps {
excludeMaterialCodes?: Set<string>; // 제외할 자재그룹코드들
showInitialData?: boolean; // 초기 클릭시 자재그룹들을 로드할지 여부
maxSelections?: number; // 최대 선택 가능한 자재 개수 (1이면 단일 선택, undefined면 제한 없음)
+ hideCode?: boolean; // 자재그룹코드 표시 여부 (보호 목적)
}
export function MaterialGroupSelector({
@@ -46,7 +47,8 @@ export function MaterialGroupSelector({
closeOnSelect = true,
excludeMaterialCodes,
showInitialData = true,
- maxSelections
+ maxSelections,
+ hideCode = false
}: MaterialGroupSelectorProps) {
const [open, setOpen] = useState(false);
@@ -72,10 +74,10 @@ export function MaterialGroupSelector({
const performSearch = useCallback(async (query: string, page: number = 1) => {
setIsSearching(true);
setSearchError(null);
-
+
try {
- const result = await searchMaterialsForSelector(query, page, 10);
-
+ const result = await searchMaterialsForSelector(query, page, 10, hideCode);
+
if (result.success) {
setSearchResults(result.data);
setPagination(result.pagination);
@@ -107,7 +109,7 @@ export function MaterialGroupSelector({
} finally {
setIsSearching(false);
}
- }, []);
+ }, [hideCode]);
// Popover 열림시 초기 데이터 로드
React.useEffect(() => {
@@ -343,14 +345,16 @@ export function MaterialGroupSelector({
</span>
)}
</div>
- <div className="text-xs text-muted-foreground">
- 자재그룹코드: {material.materialGroupCode}
- {material.materialGroupUom && (
- <span className="ml-2">
- | UOM: {material.materialGroupUom}
- </span>
- )}
- </div>
+ {!hideCode && (
+ <div className="text-xs text-muted-foreground">
+ 자재그룹코드: {material.materialGroupCode}
+ {material.materialGroupUom && (
+ <span className="ml-2">
+ | UOM: {material.materialGroupUom}
+ </span>
+ )}
+ </div>
+ )}
</div>
</CommandItem>
);
diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx
index 2dd07959..6885279a 100644
--- a/components/signup/join-form.tsx
+++ b/components/signup/join-form.tsx
@@ -71,7 +71,7 @@ import koLocale from "i18n-iso-countries/langs/ko.json";
import { getVendorTypes } from '@/lib/vendors/service';
import ConsentStep from './conset-step';
import { checkEmailExists } from '@/lib/vendor-users/service';
-import { MaterialGroupSelector } from '@/components/common/material/material-group-selector';
+import { MaterialGroupSelectorDialogMulti } from '@/components/common/material/material-group-selector-dialog-multi-without-code';
import { MaterialSearchItem } from '@/lib/material/material-group-service';
i18nIsoCountries.registerLocale(enLocale);
@@ -1232,15 +1232,12 @@ function CompleteVendorForm({
<label className="block text-sm font-medium mb-1">
{t('supplyItems')} <span className="text-red-500">*</span>
</label>
- <MaterialGroupSelector
+ <MaterialGroupSelectorDialogMulti
selectedMaterials={data.items}
- onMaterialsChange={handleMaterialsChange}
- placeholder="type material name or code..."
- noValuePlaceHolder="type material name or code..."
+ onMaterialsSelect={handleMaterialsChange}
+ placeholder="type material name..."
disabled={isSubmitting}
- singleSelect={false}
- className="w-full"
- showInitialData={false}
+ showSelectedInTrigger={true}
/>
<p className="text-xs text-gray-500 mt-1">
{t('supplyItemsHint')}
diff --git a/lib/material/material-group-service.ts b/lib/material/material-group-service.ts
index bf58d243..41f06fac 100644
--- a/lib/material/material-group-service.ts
+++ b/lib/material/material-group-service.ts
@@ -30,7 +30,8 @@ export interface MaterialSearchResult {
export async function searchMaterialsForSelector(
query: string,
page: number = 1,
- perPage: number = 10
+ perPage: number = 10,
+ hideCode: boolean = false
): Promise<MaterialSearchResult> {
try {
const offset = (page - 1) * perPage;
@@ -39,42 +40,101 @@ export async function searchMaterialsForSelector(
let searchWhere: SQL<unknown> | undefined;
if (query.trim()) {
const searchPattern = `%${query.trim()}%`;
- searchWhere = sql`(
- ${MATERIAL_GROUP_MASTER.materialGroupCode} ILIKE ${searchPattern} OR
- ${MATERIAL_GROUP_MASTER.materialGroupDescription} ILIKE ${searchPattern}
- )`;
+ if (hideCode) {
+ // 코드 보호 모드: 설명만 검색
+ searchWhere = sql`${MATERIAL_GROUP_MASTER.materialGroupDescription} ILIKE ${searchPattern}`;
+ } else {
+ // 일반 모드: 코드와 설명 모두 검색
+ searchWhere = sql`(
+ ${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({
- materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode,
- materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription,
- materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom,
- })
- .from(MATERIAL_GROUP_MASTER)
- .where(searchWhere)
- .orderBy(MATERIAL_GROUP_MASTER.materialGroupCode, MATERIAL_GROUP_MASTER.materialGroupDescription)
- .limit(perPage)
- .offset(offset);
-
- // 총 개수 조회
- const countResult = await tx
- .select({ count: sql<number>`count(*)` })
- .from(MATERIAL_GROUP_MASTER)
- .where(searchWhere);
-
- const total = countResult[0]?.count || 0;
+ let data;
+ let totalCount;
+
+ if (hideCode) {
+ // 코드 보호 모드: 같은 이름의 자재 중복 제거 (DISTINCT ON)
+ data = await tx
+ .select({
+ materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode,
+ materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription,
+ materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom,
+ })
+ .from(MATERIAL_GROUP_MASTER)
+ .where(searchWhere)
+ .orderBy(MATERIAL_GROUP_MASTER.materialGroupDescription, MATERIAL_GROUP_MASTER.materialGroupCode)
+ .$dynamic(); // Drizzle에서 DISTINCT ON을 위해 dynamic 쿼리 사용
+
+ // 실제로는 DISTINCT ON을 위해 raw SQL 사용
+ const distinctQuery = sql`
+ SELECT DISTINCT ON (material_group_description)
+ material_group_code, material_group_description, material_group_uom
+ FROM ${MATERIAL_GROUP_MASTER}
+ ${searchWhere ? sql`WHERE ${searchWhere}` : sql``}
+ ORDER BY material_group_description, material_group_code
+ LIMIT ${perPage} OFFSET ${offset}
+ `;
+
+ const distinctResult = await tx.execute(distinctQuery);
+ data = distinctResult.rows as {
+ material_group_code: string;
+ material_group_description: string;
+ material_group_uom: string | null;
+ }[];
+
+ // 총 개수도 DISTINCT로 계산
+ const countQuery = sql`
+ SELECT COUNT(DISTINCT material_group_description) as count
+ FROM ${MATERIAL_GROUP_MASTER}
+ ${searchWhere ? sql`WHERE ${searchWhere}` : sql``}
+ `;
+
+ const countResult = await tx.execute(countQuery);
+ totalCount = Number(countResult.rows[0]?.count) || 0;
+ } else {
+ // 일반 모드: 기존 방식 유지
+ data = await tx
+ .select({
+ materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode,
+ materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription,
+ materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom,
+ })
+ .from(MATERIAL_GROUP_MASTER)
+ .where(searchWhere)
+ .orderBy(MATERIAL_GROUP_MASTER.materialGroupCode, MATERIAL_GROUP_MASTER.materialGroupDescription)
+ .limit(perPage)
+ .offset(offset);
+
+ // 총 개수 조회
+ const countResult = await tx
+ .select({ count: sql<number>`count(*)` })
+ .from(MATERIAL_GROUP_MASTER)
+ .where(searchWhere);
+
+ totalCount = countResult[0]?.count || 0;
+ }
return {
- data: data.map((row) => ({
- materialGroupCode: row.materialGroupCode,
- materialGroupDescription: row.materialGroupDescription,
- materialGroupUom: row.materialGroupUom,
- displayText: `${row.materialGroupCode || ''} - ${row.materialGroupDescription || ''}`, // 애플리케이션 레벨에서 생성
+ data: data.map((row: {
+ material_group_code?: string;
+ materialGroupCode?: string;
+ material_group_description?: string;
+ materialGroupDescription?: string;
+ material_group_uom?: string | null;
+ materialGroupUom?: string | null;
+ }) => ({
+ materialGroupCode: row.material_group_code || row.materialGroupCode,
+ materialGroupDescription: row.material_group_description || row.materialGroupDescription,
+ materialGroupUom: row.material_group_uom || row.materialGroupUom,
+ displayText: hideCode
+ ? `${(row.material_group_description || row.materialGroupDescription) || ''}` // 코드 보호 모드: 설명만
+ : `${(row.material_group_code || row.materialGroupCode) || ''} - ${(row.material_group_description || row.materialGroupDescription) || ''}`, // 일반 모드: 코드 + 설명
})),
- total,
+ total: totalCount,
};
});