diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-23 09:02:07 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-23 09:02:07 +0000 |
| commit | 5c9b39eb011763a7491b3e8542de9f6d4976dd65 (patch) | |
| tree | ef18c420a72b0e4c8d5dfd03ae1e8648dda906f7 /lib | |
| parent | a75541e1a1aea596bfca2a435f39133b9b72f193 (diff) | |
(최겸) 기술영업 벤더 개발
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/tech-vendors/service.ts | 157 | ||||
| -rw-r--r-- | lib/tech-vendors/table/add-vendor-dialog.tsx | 50 | ||||
| -rw-r--r-- | lib/tech-vendors/table/excel-template-download.tsx | 4 | ||||
| -rw-r--r-- | lib/tech-vendors/table/update-vendor-sheet.tsx | 58 | ||||
| -rw-r--r-- | lib/tech-vendors/table/vendor-all-export.ts | 1 | ||||
| -rw-r--r-- | lib/tech-vendors/validations.ts | 9 |
6 files changed, 227 insertions, 52 deletions
diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts index 05ec1178..b2dec1ab 100644 --- a/lib/tech-vendors/service.ts +++ b/lib/tech-vendors/service.ts @@ -2,7 +2,7 @@ import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; -import { techVendorAttachments, techVendorContacts, techVendorPossibleItems, techVendors, techVendorItemsView, type TechVendor } from "@/db/schema/techVendors"; +import { techVendorAttachments, techVendorContacts, techVendorPossibleItems, techVendors, techVendorItemsView, type TechVendor, techVendorCandidates } from "@/db/schema/techVendors"; import { items, itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; import { filterColumns } from "@/lib/filter-columns"; @@ -272,7 +272,7 @@ export async function createTechVendor(input: CreateTechVendorSchema) { phone: input.phone || null, email: input.email, website: input.website || null, - techVendorType: input.techVendorType as "조선" | "해양TOP" | "해양HULL", + techVendorType: Array.isArray(input.techVendorType) ? input.techVendorType.join(',') : input.techVendorType, representativeName: input.representativeName || null, representativeBirth: input.representativeBirth || null, representativeEmail: input.representativeEmail || null, @@ -1066,9 +1066,9 @@ export async function exportTechVendorDetails(vendorIds: number[]) { } /** - * 기술영업 벤더 상세 정보 조회 + * 기술영업 벤더 상세 정보 조회 (연락처, 첨부파일 포함) */ -async function getTechVendorDetailById(id: number) { +export async function getTechVendorDetailById(id: number) { try { const vendor = await db.select().from(techVendors).where(eq(techVendors.id, id)).limit(1); @@ -1255,7 +1255,7 @@ export async function importTechVendorsFromExcel( phone: vendor.phone || null, email: vendor.email, website: vendor.website || null, - techVendorType: vendor.techVendorType as "조선" | "해양TOP" | "해양HULL", + techVendorType: vendor.techVendorType, status: "ACTIVE", representativeName: vendor.representativeName || null, representativeEmail: vendor.representativeEmail || null, @@ -1345,6 +1345,149 @@ export async function findTechVendorById(id: number): Promise<TechVendor | null> } /** + * 회원가입 폼을 통한 기술영업 벤더 생성 (초대 토큰 기반) + */ +export async function createTechVendorFromSignup(params: { + vendorData: { + vendorName: string + vendorCode?: string + items: string + website?: string + taxId: string + address?: string + email: string + phone?: string + country: string + techVendorType: "조선" | "해양TOP" | "해양HULL" + representativeName?: string + representativeBirth?: string + representativeEmail?: string + representativePhone?: string + } + files?: File[] + contacts: { + contactName: string + contactPosition?: string + contactEmail: string + contactPhone?: string + isPrimary?: boolean + }[] + invitationToken?: string // 초대 토큰 +}) { + unstable_noStore(); + + try { + console.log("기술영업 벤더 회원가입 시작:", params.vendorData.vendorName); + + const result = await db.transaction(async (tx) => { + // 1. 이메일 중복 체크 + const existingVendor = await tx.query.techVendors.findFirst({ + where: eq(techVendors.email, params.vendorData.email), + columns: { id: true, vendorName: true } + }); + + if (existingVendor) { + throw new Error(`이미 등록된 이메일입니다: ${params.vendorData.email}`); + } + + // 2. 벤더 생성 + const [newVendor] = await tx.insert(techVendors).values({ + vendorName: params.vendorData.vendorName, + vendorCode: params.vendorData.vendorCode || null, + taxId: params.vendorData.taxId, + country: params.vendorData.country, + address: params.vendorData.address || null, + phone: params.vendorData.phone || null, + email: params.vendorData.email, + website: params.vendorData.website || null, + techVendorType: params.vendorData.techVendorType, + status: "ACTIVE", + representativeName: params.vendorData.representativeName || null, + representativeEmail: params.vendorData.representativeEmail || null, + representativePhone: params.vendorData.representativePhone || null, + representativeBirth: params.vendorData.representativeBirth || null, + items: params.vendorData.items, + }).returning(); + + console.log("기술영업 벤더 생성 성공:", newVendor.id); + + // 3. 연락처 생성 + if (params.contacts && params.contacts.length > 0) { + for (const [index, contact] of params.contacts.entries()) { + await tx.insert(techVendorContacts).values({ + vendorId: newVendor.id, + contactName: contact.contactName, + contactPosition: contact.contactPosition || null, + contactEmail: contact.contactEmail, + contactPhone: contact.contactPhone || null, + isPrimary: index === 0, // 첫 번째 연락처를 primary로 설정 + }); + } + console.log("연락처 생성 완료:", params.contacts.length, "개"); + } + + // 4. 첨부파일 처리 + if (params.files && params.files.length > 0) { + await storeTechVendorFiles(tx, newVendor.id, params.files, "GENERAL"); + console.log("첨부파일 저장 완료:", params.files.length, "개"); + } + + // 5. 유저 생성 (techCompanyId 설정) + console.log("유저 생성 시도:", params.vendorData.email); + + const existingUser = await tx.query.users.findFirst({ + where: eq(users.email, params.vendorData.email), + columns: { id: true, techCompanyId: true } + }); + + let userId = null; + if (!existingUser) { + const [newUser] = await tx.insert(users).values({ + name: params.vendorData.vendorName, + email: params.vendorData.email, + techCompanyId: newVendor.id, // 중요: techCompanyId 설정 + domain: "partners", + }).returning(); + userId = newUser.id; + console.log("유저 생성 성공:", userId); + } else { + // 기존 유저의 techCompanyId 업데이트 + if (!existingUser.techCompanyId) { + await tx.update(users) + .set({ techCompanyId: newVendor.id }) + .where(eq(users.id, existingUser.id)); + console.log("기존 유저의 techCompanyId 업데이트:", existingUser.id); + } + userId = existingUser.id; + } + + // 6. 후보에서 해당 이메일이 있으면 vendorId 업데이트 및 상태 변경 + if (params.vendorData.email) { + await tx.update(techVendorCandidates) + .set({ + vendorId: newVendor.id, + status: "INVITED" + }) + .where(eq(techVendorCandidates.contactEmail, params.vendorData.email)); + } + + return { vendor: newVendor, userId }; + }); + + // 캐시 무효화 + revalidateTag("tech-vendors"); + revalidateTag("tech-vendor-candidates"); + revalidateTag("users"); + + console.log("기술영업 벤더 회원가입 완료:", result); + return { success: true, data: result }; + } catch (error) { + console.error("기술영업 벤더 회원가입 실패:", error); + return { success: false, error: getErrorMessage(error) }; + } +} + +/** * 단일 기술영업 벤더 추가 (사용자 계정도 함께 생성) */ export async function addTechVendor(input: { @@ -1361,7 +1504,7 @@ export async function addTechVendor(input: { address?: string | null; phone?: string | null; website?: string | null; - techVendorType: "조선" | "해양TOP" | "해양HULL"; + techVendorType: string; representativeName?: string | null; representativeEmail?: string | null; representativePhone?: string | null; @@ -1404,7 +1547,7 @@ export async function addTechVendor(input: { phone: input.phone || null, email: input.email, website: input.website || null, - techVendorType: input.techVendorType, + techVendorType: Array.isArray(input.techVendorType) ? input.techVendorType.join(',') : input.techVendorType, status: "ACTIVE", representativeName: input.representativeName || null, representativeEmail: input.representativeEmail || null, diff --git a/lib/tech-vendors/table/add-vendor-dialog.tsx b/lib/tech-vendors/table/add-vendor-dialog.tsx index bc260d51..da9880d4 100644 --- a/lib/tech-vendors/table/add-vendor-dialog.tsx +++ b/lib/tech-vendors/table/add-vendor-dialog.tsx @@ -25,13 +25,7 @@ import { FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
+
import { Textarea } from "@/components/ui/textarea"
import { Plus, Loader2 } from "lucide-react"
@@ -52,9 +46,7 @@ const addVendorSchema = z.object({ address: z.string().optional(),
phone: z.string().optional(),
website: z.string().optional(),
- techVendorType: z.enum(["조선", "해양TOP", "해양HULL"], {
- required_error: "벤더 타입을 선택해주세요",
- }),
+ techVendorType: z.array(z.enum(["조선", "해양TOP", "해양HULL"])).min(1, "최소 하나의 벤더 타입을 선택해주세요"),
representativeName: z.string().optional(),
representativeEmail: z.string().email("올바른 이메일 주소를 입력해주세요").optional().or(z.literal("")),
representativePhone: z.string().optional(),
@@ -87,7 +79,7 @@ export function AddVendorDialog({ onSuccess }: AddVendorDialogProps) { address: "",
phone: "",
website: "",
- techVendorType: undefined,
+ techVendorType: [],
representativeName: "",
representativeEmail: "",
representativePhone: "",
@@ -110,6 +102,7 @@ export function AddVendorDialog({ onSuccess }: AddVendorDialogProps) { address: data.address || null,
phone: data.phone || null,
website: data.website || null,
+ techVendorType: data.techVendorType.join(','),
representativeName: data.representativeName || null,
representativeEmail: data.representativeEmail || null,
representativePhone: data.representativePhone || null,
@@ -218,18 +211,29 @@ export function AddVendorDialog({ onSuccess }: AddVendorDialogProps) { render={({ field }) => (
<FormItem>
<FormLabel>벤더 타입 *</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="벤더 타입을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="조선">조선</SelectItem>
- <SelectItem value="해양TOP">해양TOP</SelectItem>
- <SelectItem value="해양HULL">해양HULL</SelectItem>
- </SelectContent>
- </Select>
+ <div className="space-y-2">
+ {["조선", "해양TOP", "해양HULL"].map((type) => (
+ <div key={type} className="flex items-center space-x-2">
+ <input
+ type="checkbox"
+ id={type}
+ checked={field.value?.includes(type as "조선" | "해양TOP" | "해양HULL")}
+ onChange={(e) => {
+ const currentValue = field.value || [];
+ if (e.target.checked) {
+ field.onChange([...currentValue, type]);
+ } else {
+ field.onChange(currentValue.filter((v) => v !== type));
+ }
+ }}
+ className="w-4 h-4"
+ />
+ <label htmlFor={type} className="text-sm font-medium cursor-pointer">
+ {type}
+ </label>
+ </div>
+ ))}
+ </div>
<FormMessage />
</FormItem>
)}
diff --git a/lib/tech-vendors/table/excel-template-download.tsx b/lib/tech-vendors/table/excel-template-download.tsx index db2c5fb5..b6011e2c 100644 --- a/lib/tech-vendors/table/excel-template-download.tsx +++ b/lib/tech-vendors/table/excel-template-download.tsx @@ -72,7 +72,7 @@ export async function exportTechVendorTemplate() { phone: '02-1234-5678', email: 'sample1@example.com', website: 'https://example1.com', - techVendorType: '조선', + techVendorType: '조선,해양TOP', representativeName: '홍길동', representativeEmail: 'ceo1@example.com', representativePhone: '010-1234-5678', @@ -93,7 +93,7 @@ export async function exportTechVendorTemplate() { phone: '051-234-5678', email: 'sample2@example.com', website: 'https://example2.com', - techVendorType: '해양TOP', + techVendorType: '해양HULL', representativeName: '김철수', representativeEmail: 'ceo2@example.com', representativePhone: '010-2345-6789', diff --git a/lib/tech-vendors/table/update-vendor-sheet.tsx b/lib/tech-vendors/table/update-vendor-sheet.tsx index cc6b4003..774299f1 100644 --- a/lib/tech-vendors/table/update-vendor-sheet.tsx +++ b/lib/tech-vendors/table/update-vendor-sheet.tsx @@ -65,24 +65,6 @@ type StatusConfig = { // 상태 표시 유틸리티 함수 const getStatusConfig = (status: StatusType): StatusConfig => { switch(status) { - case "PENDING_REVIEW": - return { - Icon: ClipboardList, - className: "text-yellow-600", - label: "가입 신청 중" - }; - case "IN_REVIEW": - return { - Icon: FilePenLine, - className: "text-blue-600", - label: "심사 중" - }; - case "REJECTED": - return { - Icon: XCircle, - className: "text-red-600", - label: "심사 거부됨" - }; case "ACTIVE": return { Icon: Activity, @@ -127,6 +109,7 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", + techVendorType: vendor?.techVendorType ? vendor.techVendorType.split(',').filter(Boolean) : [], status: vendor?.status ?? "ACTIVE", }, }) @@ -141,6 +124,7 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", + techVendorType: vendor?.techVendorType ? vendor.techVendorType.split(',').filter(Boolean) : [], status: vendor?.status ?? "ACTIVE", }); @@ -172,7 +156,8 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) id: String(vendor.id), userId: Number(session.user.id), // Add user ID from session comment: statusComment, // Add comment for status changes - ...data // 모든 데이터 전달 - 서비스 함수에서 필요한 필드만 처리 + ...data, // 모든 데이터 전달 - 서비스 함수에서 필요한 필드만 처리 + techVendorType: data.techVendorType ? data.techVendorType.join(',') : undefined, }) if (error) throw new Error(error) @@ -312,6 +297,41 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) )} /> + {/* techVendorType */} + <FormField + control={form.control} + name="techVendorType" + render={({ field }) => ( + <FormItem className="md:col-span-2"> + <FormLabel>벤더 타입 *</FormLabel> + <div className="space-y-2"> + {["조선", "해양TOP", "해양HULL"].map((type) => ( + <div key={type} className="flex items-center space-x-2"> + <input + type="checkbox" + id={`update-${type}`} + checked={field.value?.includes(type as "조선" | "해양TOP" | "해양HULL")} + onChange={(e) => { + const currentValue = field.value || []; + if (e.target.checked) { + field.onChange([...currentValue, type]); + } else { + field.onChange(currentValue.filter((v) => v !== type)); + } + }} + className="w-4 h-4" + /> + <label htmlFor={`update-${type}`} className="text-sm font-medium cursor-pointer"> + {type} + </label> + </div> + ))} + </div> + <FormMessage /> + </FormItem> + )} + /> + {/* status with icons */} <FormField control={form.control} diff --git a/lib/tech-vendors/table/vendor-all-export.ts b/lib/tech-vendors/table/vendor-all-export.ts index a1ad4fd1..f2650102 100644 --- a/lib/tech-vendors/table/vendor-all-export.ts +++ b/lib/tech-vendors/table/vendor-all-export.ts @@ -108,6 +108,7 @@ function createBasicInfoSheet( address: vendor.address, representativeName: vendor.representativeName, createdAt: vendor.createdAt ? formatDate(vendor.createdAt) : "", + techVendorType: vendor.techVendorType?.split(',').join(', ') || vendor.techVendorType, }); }); } diff --git a/lib/tech-vendors/validations.ts b/lib/tech-vendors/validations.ts index bae3e5b4..c45eb97d 100644 --- a/lib/tech-vendors/validations.ts +++ b/lib/tech-vendors/validations.ts @@ -117,6 +117,10 @@ export const updateTechVendorSchema = z.object({ phone: z.string().optional(), email: z.string().email("유효한 이메일 주소를 입력해주세요").optional(), website: z.string().url("유효한 URL을 입력해주세요").optional(), + techVendorType: z.union([ + z.array(z.enum(VENDOR_TYPES)).min(1, "최소 하나의 벤더 타입을 선택해주세요"), + z.string().min(1, "벤더 타입을 선택해주세요") + ]).optional(), status: z.enum(techVendors.status.enumValues).optional(), userId: z.number().optional(), comment: z.string().optional(), @@ -155,7 +159,10 @@ export const createTechVendorSchema = z files: z.any().optional(), status: z.enum(techVendors.status.enumValues).default("ACTIVE"), - techVendorType: z.enum(VENDOR_TYPES).default("조선"), + techVendorType: z.union([ + z.array(z.enum(VENDOR_TYPES)).min(1, "최소 하나의 벤더 타입을 선택해주세요"), + z.string().min(1, "벤더 타입을 선택해주세요") + ]).default(["조선"]), representativeName: z.union([z.string().max(255), z.literal("")]).optional(), representativeBirth: z.union([z.string().max(20), z.literal("")]).optional(), |
