summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-03 02:50:02 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-03 02:50:02 +0000
commit766f95945a7ca0fdb258d6a83229593e4fcccfa6 (patch)
tree3374364b5a90b2704d7ebd18ab404bf6e8fe69e2
parente4b2bef735e6aab6a5ecae9a017c5c618a6d3a4b (diff)
(최겸) 기술영업 RFQ 견적 프로젝트별 생성 기능 추가
-rw-r--r--lib/items-tech/service.ts59
-rw-r--r--lib/items-tech/table/import-excel-button.tsx6
-rw-r--r--lib/techsales-rfq/service.ts74
-rw-r--r--lib/techsales-rfq/table/create-rfq-hull-dialog.tsx4
-rw-r--r--lib/techsales-rfq/table/create-rfq-ship-dialog.tsx8
-rw-r--r--lib/techsales-rfq/table/create-rfq-top-dialog.tsx2
6 files changed, 115 insertions, 38 deletions
diff --git a/lib/items-tech/service.ts b/lib/items-tech/service.ts
index be65f5dd..0cc08d23 100644
--- a/lib/items-tech/service.ts
+++ b/lib/items-tech/service.ts
@@ -13,21 +13,21 @@ import { GetShipbuildingSchema, GetOffshoreTopSchema, GetOffshoreHullSchema, Shi
import { itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items";
// 타입 정의 추가
-type WorkType = '기장' | '전장' | '선실' | '배관' | '철의';
-type OffshoreTopWorkType = 'TM' | 'TS' | 'TE' | 'TP';
-type OffshoreHullWorkType = 'HA' | 'HE' | 'HH' | 'HM' | 'NC';
+export type ShipbuildingWorkType = '기장' | '전장' | '선실' | '배관' | '철의';
+export type OffshoreTopWorkType = 'TM' | 'TS' | 'TE' | 'TP';
+export type OffshoreHullWorkType = 'HA' | 'HE' | 'HH' | 'HM' | 'NC';
-interface ShipbuildingItem {
+export interface ShipbuildingItem {
id: number;
itemCode: string;
- workType: WorkType;
+ workType: ShipbuildingWorkType;
itemList: string;
shipTypes: string;
createdAt: Date;
updatedAt: Date;
}
-interface OffshoreTopTechItem {
+export interface OffshoreTopTechItem {
id: number;
itemCode: string;
workType: OffshoreTopWorkType;
@@ -37,7 +37,7 @@ interface OffshoreTopTechItem {
updatedAt: Date;
}
-interface OffshoreHullTechItem {
+export interface OffshoreHullTechItem {
id: number;
itemCode: string;
workType: OffshoreHullWorkType;
@@ -409,14 +409,19 @@ export async function createShipbuildingImportItem(input: {
}
}
- // 기존 아이템 확인
+ // 기존 아이템 및 선종 확인
const existingItem = await db.select().from(itemShipbuilding)
- .where(eq(itemShipbuilding.itemCode, input.itemCode));
+ .where(
+ and(
+ eq(itemShipbuilding.itemCode, input.itemCode),
+ eq(itemShipbuilding.shipTypes, input.shipTypes || '')
+ )
+ );
if (existingItem.length > 0) {
return {
success: false,
- message: "이미 존재하는 아이템 코드입니다",
+ message: "이미 존재하는 아이템 코드 및 선종입니다",
data: null,
error: "중복 키 오류"
}
@@ -444,7 +449,7 @@ export async function createShipbuildingImportItem(input: {
if (err instanceof Error && err.message.includes("unique constraint")) {
return {
success: false,
- message: "이미 존재하는 아이템 코드입니다",
+ message: "이미 존재하는 아이템 코드 및 선종입니다",
data: null,
error: "중복 키 오류"
}
@@ -845,7 +850,7 @@ export async function removeOffshoreHullItems(input: DeleteItemsInput) {
----------------------------------------------------- */
// 조선 공종별 아이템 조회
-export async function getShipbuildingItemsByWorkType(workType?: WorkType, shipType?: string) {
+export async function getShipbuildingItemsByWorkType(workType?: ShipbuildingWorkType, shipType?: string) {
try {
const query = db
.select({
@@ -955,7 +960,7 @@ export async function getOffshoreHullItemsByWorkType(workType?: OffshoreHullWork
}
// 아이템 검색
-export async function searchShipbuildingItems(searchQuery: string, workType?: WorkType, shipType?: string) {
+export async function searchShipbuildingItems(searchQuery: string, workType?: ShipbuildingWorkType, shipType?: string) {
try {
const searchConditions = [
ilike(itemShipbuilding.itemCode, `%${searchQuery}%`),
@@ -1096,32 +1101,32 @@ export async function searchOffshoreHullItems(searchQuery: string, workType?: Of
// 모든 공종 목록 조회
export async function getWorkTypes() {
return [
- { code: '기장' as WorkType, name: '기장', description: '기계 장치' },
- { code: '전장' as WorkType, name: '전장', description: '전기 장치' },
- { code: '선실' as WorkType, name: '선실', description: '선실' },
- { code: '배관' as WorkType, name: '배관', description: '배관' },
- { code: '철의' as WorkType, name: '철의', description: '선체 강재' },
+ { code: '기장' as ShipbuildingWorkType, name: '기장'},
+ { code: '전장' as ShipbuildingWorkType, name: '전장'},
+ { code: '선실' as ShipbuildingWorkType, name: '선실'},
+ { code: '배관' as ShipbuildingWorkType, name: '배관'},
+ { code: '철의' as ShipbuildingWorkType, name: '철의'},
]
}
// 해양 TOP 공종 목록 조회
export async function getOffshoreTopWorkTypes() {
return [
- { code: 'TM' as OffshoreTopWorkType, name: 'TM', description: 'Topside Manufacturing' },
- { code: 'TS' as OffshoreTopWorkType, name: 'TS', description: 'Topside Steel' },
- { code: 'TE' as OffshoreTopWorkType, name: 'TE', description: 'Topside Equipment' },
- { code: 'TP' as OffshoreTopWorkType, name: 'TP', description: 'Topside Piping' },
+ { code: 'TM' as OffshoreTopWorkType, name: 'TM'},
+ { code: 'TS' as OffshoreTopWorkType, name: 'TS'},
+ { code: 'TE' as OffshoreTopWorkType, name: 'TE'},
+ { code: 'TP' as OffshoreTopWorkType, name: 'TP'},
]
}
// 해양 HULL 공종 목록 조회
export async function getOffshoreHullWorkTypes() {
return [
- { code: 'HA' as OffshoreHullWorkType, name: 'HA', description: 'Hull Assembly' },
- { code: 'HE' as OffshoreHullWorkType, name: 'HE', description: 'Hull Equipment' },
- { code: 'HH' as OffshoreHullWorkType, name: 'HH', description: 'Hull Heating' },
- { code: 'HM' as OffshoreHullWorkType, name: 'HM', description: 'Hull Manufacturing' },
- { code: 'NC' as OffshoreHullWorkType, name: 'NC', description: 'No Category' },
+ { code: 'HA' as OffshoreHullWorkType, name: 'HA'},
+ { code: 'HE' as OffshoreHullWorkType, name: 'HE'},
+ { code: 'HH' as OffshoreHullWorkType, name: 'HH'},
+ { code: 'HM' as OffshoreHullWorkType, name: 'HM'},
+ { code: 'NC' as OffshoreHullWorkType, name: 'NC'},
]
}
diff --git a/lib/items-tech/table/import-excel-button.tsx b/lib/items-tech/table/import-excel-button.tsx
index 3281823c..02736664 100644
--- a/lib/items-tech/table/import-excel-button.tsx
+++ b/lib/items-tech/table/import-excel-button.tsx
@@ -102,7 +102,7 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps)
worksheet.eachRow((row, rowNumber) => {
const values = row.values as (string | null)[];
- if (!headerRow && values.some(v => v === "아이템 코드" || v === "itemCode" || v === "item_code")) {
+ if (!headerRow && values.some(v => v === "자재 그룹" || v === "itemCode" || v === "item_code")) {
headerRowIndex = rowNumber;
headerRow = row;
headerValues = [...values];
@@ -122,10 +122,10 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps)
});
// 필수 헤더 확인 (타입별 구분)
- const requiredHeaders: string[] = ["아이템 코드", "기능(공종)"];
+ const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"];
const alternativeHeaders = {
- "아이템 코드": ["itemCode", "item_code"],
+ "자재 그룹": ["itemCode", "item_code"],
"기능(공종)": ["workType"],
"자재명": ["itemList"],
"자재명(상세)": ["subItemList"]
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index e658747b..d5cb8efe 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -39,7 +39,12 @@ import { decryptWithServerAction } from "@/components/drm/drmUtils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type OrderByType = any;
-
+export type Project = {
+ id: number;
+ projectCode: string;
+ projectName: string;
+ pjtType: "SHIP" | "TOP" | "HULL";
+}
/**
* 연도별 순차 RFQ 코드 생성 함수 (다중 생성 지원)
@@ -2745,6 +2750,40 @@ export async function addTechVendorsToTechSalesRfq(input: {
})
.returning({ id: techSalesVendorQuotations.id });
+ // 🆕 RFQ의 아이템 코드들을 tech_vendor_possible_items에 추가
+ try {
+ // RFQ의 아이템들 조회
+ const rfqItemsResult = await getTechSalesRfqItems(input.rfqId);
+
+ if (rfqItemsResult.data && rfqItemsResult.data.length > 0) {
+ const itemCodes = rfqItemsResult.data
+ .map(item => item.itemCode)
+ .filter(code => code); // 빈 코드 제외
+
+ // 각 아이템 코드에 대해 tech_vendor_possible_items에 추가 (중복 체크)
+ for (const itemCode of itemCodes) {
+ // 이미 존재하는지 확인
+ const existing = await tx.query.techVendorPossibleItems.findFirst({
+ where: and(
+ eq(techVendorPossibleItems.vendorId, vendorId),
+ eq(techVendorPossibleItems.itemCode, itemCode)
+ )
+ });
+
+ // 존재하지 않으면 추가
+ if (!existing) {
+ await tx.insert(techVendorPossibleItems).values({
+ vendorId: vendorId,
+ itemCode: itemCode,
+ });
+ }
+ }
+ }
+ } catch (possibleItemError) {
+ // tech_vendor_possible_items 추가 실패는 전체 실패로 처리하지 않음
+ console.warn(`벤더 ${vendorId}의 가능 아이템 추가 실패:`, possibleItemError);
+ }
+
results.push({ id: quotation.id, vendorId, vendorName: vendor.vendorName });
} catch (vendorError) {
console.error(`Error adding vendor ${vendorId}:`, vendorError);
@@ -3379,4 +3418,37 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
console.error("getAcceptedTechSalesVendorQuotations 오류:", error);
throw new Error(`Accepted quotations 조회 실패: ${getErrorMessage(error)}`);
}
+}
+
+export async function getBidProjects(pjtType: 'SHIP' | 'TOP' | 'HULL'): Promise<Project[]> {
+ try {
+ // 트랜잭션을 사용하여 프로젝트 데이터 조회
+ const projectList = await db.transaction(async (tx) => {
+ // 기본 쿼리 구성
+ const query = tx
+ .select({
+ id: biddingProjects.id,
+ projectCode: biddingProjects.pspid,
+ projectName: biddingProjects.projNm,
+ pjtType: biddingProjects.pjtType,
+ })
+ .from(biddingProjects)
+ .where(eq(biddingProjects.pjtType, pjtType));
+
+ const results = await query.orderBy(biddingProjects.id);
+ return results;
+ });
+
+ // Handle null projectName values and ensure pjtType is not null
+ const validProjectList = projectList.map(project => ({
+ ...project,
+ projectName: project.projectName || '', // Replace null with empty string
+ pjtType: project.pjtType as "SHIP" | "TOP" | "HULL" // Type assertion since WHERE filters ensure non-null
+ }));
+
+ return validProjectList;
+ } catch (error) {
+ console.error("프로젝트 목록 가져오기 실패:", error);
+ return []; // 오류 발생 시 빈 배열 반환
+ }
} \ No newline at end of file
diff --git a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx
index 7bbbfa75..23c57491 100644
--- a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx
+++ b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx
@@ -57,7 +57,7 @@ import {
getOffshoreHullWorkTypes,
getAllOffshoreHullItemsForCache,
type OffshoreHullWorkType,
- type OffshoreHullTechItem
+ type OffshoreHullTechItem,
} from "@/lib/items-tech/service"
// 해양 HULL 아이템 타입 정의 (이미 service에서 import하므로 제거)
@@ -83,7 +83,6 @@ type CreateHullRfqFormValues = z.infer<typeof createHullRfqSchema>
interface WorkTypeOption {
code: OffshoreHullWorkType
name: string
- description: string
}
interface CreateHullRfqDialogProps {
@@ -355,6 +354,7 @@ export function CreateHullRfqDialog({ onCreated }: CreateHullRfqDialogProps) {
selectedProjectId={field.value}
onProjectSelect={handleProjectSelect}
placeholder="입찰 프로젝트를 선택하세요"
+ pjtType="HULL"
/>
</FormControl>
<FormMessage />
diff --git a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx
index b616f526..efa4e164 100644
--- a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx
+++ b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx
@@ -50,7 +50,7 @@ import {
getAllShipbuildingItemsForCache,
getShipTypes,
type ShipbuildingItem,
- type WorkType
+ type ShipbuildingWorkType
} from "@/lib/items-tech/service"
@@ -73,9 +73,8 @@ type CreateShipRfqFormValues = z.infer<typeof createShipRfqSchema>
// 공종 타입 정의
interface WorkTypeOption {
- code: WorkType
+ code: ShipbuildingWorkType
name: string
- description: string
}
interface CreateShipRfqDialogProps {
@@ -90,7 +89,7 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) {
// 검색 및 필터링 상태
const [itemSearchQuery, setItemSearchQuery] = React.useState("")
- const [selectedWorkType, setSelectedWorkType] = React.useState<WorkType | null>(null)
+ const [selectedWorkType, setSelectedWorkType] = React.useState<ShipbuildingWorkType | null>(null)
const [selectedShipType, setSelectedShipType] = React.useState<string | null>(null)
const [selectedItems, setSelectedItems] = React.useState<ShipbuildingItem[]>([])
@@ -370,6 +369,7 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) {
selectedProjectId={field.value}
onProjectSelect={handleProjectSelect}
placeholder="입찰 프로젝트를 선택하세요"
+ pjtType="SHIP"
/>
</FormControl>
<FormMessage />
diff --git a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx
index 6536e230..ef2229ac 100644
--- a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx
+++ b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx
@@ -75,7 +75,6 @@ type CreateTopRfqFormValues = z.infer<typeof createTopRfqSchema>
interface WorkTypeOption {
code: OffshoreTopWorkType
name: string
- description: string
}
interface CreateTopRfqDialogProps {
@@ -346,6 +345,7 @@ export function CreateTopRfqDialog({ onCreated }: CreateTopRfqDialogProps) {
selectedProjectId={field.value}
onProjectSelect={handleProjectSelect}
placeholder="입찰 프로젝트를 선택하세요"
+ pjtType="TOP"
/>
</FormControl>
<FormMessage />