summaryrefslogtreecommitdiff
path: root/lib/tech-vendors
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors')
-rw-r--r--lib/tech-vendors/service.ts157
-rw-r--r--lib/tech-vendors/table/add-vendor-dialog.tsx50
-rw-r--r--lib/tech-vendors/table/excel-template-download.tsx4
-rw-r--r--lib/tech-vendors/table/update-vendor-sheet.tsx58
-rw-r--r--lib/tech-vendors/table/vendor-all-export.ts1
-rw-r--r--lib/tech-vendors/validations.ts9
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(),