summaryrefslogtreecommitdiff
path: root/lib/vendors
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
commit8b23b471638a155fd1bfa3a8c853b26d9315b272 (patch)
tree47353e9dd342011cb2f1dcd24b09661707a8421b /lib/vendors
parentd62368d2b68d73da895977e60a18f9b1286b0545 (diff)
(대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등
(최겸) 입찰
Diffstat (limited to 'lib/vendors')
-rw-r--r--lib/vendors/contacts-table/add-contact-dialog.tsx81
-rw-r--r--lib/vendors/contacts-table/contact-table.tsx25
-rw-r--r--lib/vendors/contacts-table/edit-contact-dialog.tsx231
-rw-r--r--lib/vendors/repository.ts20
-rw-r--r--lib/vendors/service.ts34
-rw-r--r--lib/vendors/validations.ts24
6 files changed, 387 insertions, 28 deletions
diff --git a/lib/vendors/contacts-table/add-contact-dialog.tsx b/lib/vendors/contacts-table/add-contact-dialog.tsx
index 5376583a..22c557b4 100644
--- a/lib/vendors/contacts-table/add-contact-dialog.tsx
+++ b/lib/vendors/contacts-table/add-contact-dialog.tsx
@@ -8,6 +8,13 @@ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, Dialog
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
Form,
FormControl,
FormField,
@@ -29,6 +36,20 @@ interface AddContactDialogProps {
export function AddContactDialog({ vendorId }: AddContactDialogProps) {
const [open, setOpen] = React.useState(false)
+ // 담당업무 옵션
+ const taskOptions = [
+ { value: "회사대표", label: "회사대표 President/Director" },
+ { value: "영업관리", label: "영업관리 Sales Management" },
+ { value: "설계/기술", label: "설계/기술 Engineering/Design" },
+ { value: "구매", label: "구매 Procurement" },
+ { value: "납기/출하/운송", label: "납기/출하/운송 Delivery Control" },
+ { value: "PM/생산관리", label: "PM/생산관리 PM/Manufacturing" },
+ { value: "품질관리", label: "품질관리 Quality Management" },
+ { value: "세금계산서/납품서관리", label: "세금계산서/납품서관리 Shipping Doc. Management" },
+ { value: "A/S 관리", label: "A/S 관리 A/S Management" },
+ { value: "FSE", label: "FSE(야드작업자) Field Service Engineer" }
+ ]
+
// react-hook-form 세팅
const form = useForm<CreateVendorContactSchema>({
resolver: zodResolver(createVendorContactSchema),
@@ -37,6 +58,8 @@ export function AddContactDialog({ vendorId }: AddContactDialogProps) {
vendorId,
contactName: "",
contactPosition: "",
+ contactDepartment: "",
+ contactTask: "",
contactEmail: "",
contactPhone: "",
isPrimary: false,
@@ -88,7 +111,7 @@ export function AddContactDialog({ vendorId }: AddContactDialogProps) {
name="contactName"
render={({ field }) => (
<FormItem>
- <FormLabel>Contact Name</FormLabel>
+ <FormLabel>담당자명</FormLabel>
<FormControl>
<Input placeholder="예: 홍길동" {...field} />
</FormControl>
@@ -102,7 +125,7 @@ export function AddContactDialog({ vendorId }: AddContactDialogProps) {
name="contactPosition"
render={({ field }) => (
<FormItem>
- <FormLabel>Position / Title</FormLabel>
+ <FormLabel>직급</FormLabel>
<FormControl>
<Input placeholder="예: 과장" {...field} />
</FormControl>
@@ -113,12 +136,12 @@ export function AddContactDialog({ vendorId }: AddContactDialogProps) {
<FormField
control={form.control}
- name="contactEmail"
+ name="contactDepartment"
render={({ field }) => (
<FormItem>
- <FormLabel>Email</FormLabel>
+ <FormLabel>부서</FormLabel>
<FormControl>
- <Input placeholder="name@company.com" {...field} />
+ <Input placeholder="예: 영업부" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -127,36 +150,58 @@ export function AddContactDialog({ vendorId }: AddContactDialogProps) {
<FormField
control={form.control}
- name="contactPhone"
+ name="contactTask"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당업무</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value || undefined}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="담당업무를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {taskOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactEmail"
render={({ field }) => (
<FormItem>
- <FormLabel>Phone</FormLabel>
+ <FormLabel>이메일</FormLabel>
<FormControl>
- <Input placeholder="010-1234-5678" {...field} />
+ <Input placeholder="name@company.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
- {/* 단순 checkbox */}
<FormField
control={form.control}
- name="isPrimary"
+ name="contactPhone"
render={({ field }) => (
<FormItem>
- <div className="flex items-center space-x-2 mt-2">
- <input
- type="checkbox"
- checked={field.value}
- onChange={(e) => field.onChange(e.target.checked)}
- />
- <FormLabel>Is Primary?</FormLabel>
- </div>
+ <FormLabel>전화번호</FormLabel>
+ <FormControl>
+ <Input placeholder="010-1234-5678" {...field} />
+ </FormControl>
<FormMessage />
</FormItem>
)}
/>
+
+
</div>
<DialogFooter>
diff --git a/lib/vendors/contacts-table/contact-table.tsx b/lib/vendors/contacts-table/contact-table.tsx
index 2991187e..65b12451 100644
--- a/lib/vendors/contacts-table/contact-table.tsx
+++ b/lib/vendors/contacts-table/contact-table.tsx
@@ -16,6 +16,7 @@ import { getColumns } from "./contact-table-columns"
import { getVendorContacts, } from "../service"
import { VendorContact, vendors } from "@/db/schema/vendors"
import { VendorsTableToolbarActions } from "./contact-table-toolbar-actions"
+import { EditContactDialog } from "./edit-contact-dialog"
interface VendorsTableProps {
promises: Promise<
@@ -33,6 +34,23 @@ export function VendorContactsTable({ promises , vendorId}: VendorsTableProps) {
const [{ data, pageCount }] = React.use(promises)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorContact> | null>(null)
+ const [editDialogOpen, setEditDialogOpen] = React.useState(false)
+ const [selectedContact, setSelectedContact] = React.useState<VendorContact | null>(null)
+
+ // Edit 액션 처리
+ React.useEffect(() => {
+ if (rowAction?.type === "update") {
+ setSelectedContact(rowAction.row.original)
+ setEditDialogOpen(true)
+ setRowAction(null)
+ }
+ }, [rowAction])
+
+ // 데이터 새로고침 함수
+ const handleEditSuccess = React.useCallback(() => {
+ // 페이지를 새로고침하거나 데이터를 다시 가져오기
+ window.location.reload()
+ }, [])
// getColumns() 호출 시, router를 주입
const columns = React.useMemo(
@@ -82,6 +100,13 @@ export function VendorContactsTable({ promises , vendorId}: VendorsTableProps) {
<VendorsTableToolbarActions table={table} vendorId={vendorId} />
</DataTableAdvancedToolbar>
</DataTable>
+
+ <EditContactDialog
+ contact={selectedContact}
+ open={editDialogOpen}
+ onOpenChange={setEditDialogOpen}
+ onSuccess={handleEditSuccess}
+ />
</>
)
} \ No newline at end of file
diff --git a/lib/vendors/contacts-table/edit-contact-dialog.tsx b/lib/vendors/contacts-table/edit-contact-dialog.tsx
new file mode 100644
index 00000000..e123568e
--- /dev/null
+++ b/lib/vendors/contacts-table/edit-contact-dialog.tsx
@@ -0,0 +1,231 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+
+import {
+ updateVendorContactSchema,
+ type UpdateVendorContactSchema,
+} from "../validations"
+import { updateVendorContact } from "../service"
+import { VendorContact } from "@/db/schema/vendors"
+
+interface EditContactDialogProps {
+ contact: VendorContact | null
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSuccess: () => void
+}
+
+export function EditContactDialog({ contact, open, onOpenChange, onSuccess }: EditContactDialogProps) {
+ // 담당업무 옵션
+ const taskOptions = [
+ { value: "회사대표", label: "회사대표 President/Director" },
+ { value: "영업관리", label: "영업관리 Sales Management" },
+ { value: "설계/기술", label: "설계/기술 Engineering/Design" },
+ { value: "구매", label: "구매 Procurement" },
+ { value: "납기/출하/운송", label: "납기/출하/운송 Delivery Control" },
+ { value: "PM/생산관리", label: "PM/생산관리 PM/Manufacturing" },
+ { value: "품질관리", label: "품질관리 Quality Management" },
+ { value: "세금계산서/납품서관리", label: "세금계산서/납품서관리 Shipping Doc. Management" },
+ { value: "A/S 관리", label: "A/S 관리 A/S Management" },
+ { value: "FSE", label: "FSE(야드작업자) Field Service Engineer" }
+ ]
+
+ // react-hook-form 세팅
+ const form = useForm<UpdateVendorContactSchema>({
+ resolver: zodResolver(updateVendorContactSchema),
+ defaultValues: {
+ contactName: "",
+ contactPosition: "",
+ contactDepartment: "",
+ contactTask: "",
+ contactEmail: "",
+ contactPhone: "",
+ isPrimary: false,
+ },
+ })
+
+ // contact가 변경되면 폼 초기화
+ React.useEffect(() => {
+ if (contact) {
+ form.reset({
+ contactName: contact.contactName || "",
+ contactPosition: contact.contactPosition || "",
+ contactDepartment: contact.contactDepartment || "",
+ contactTask: contact.contactTask || "",
+ contactEmail: contact.contactEmail || "",
+ contactPhone: contact.contactPhone || "",
+ isPrimary: contact.isPrimary || false,
+ })
+ }
+ }, [contact, form])
+
+ async function onSubmit(data: UpdateVendorContactSchema) {
+ if (!contact) return
+
+ const result = await updateVendorContact(contact.id, data)
+ if (result.error) {
+ alert(`에러: ${result.error}`)
+ return
+ }
+
+ // 성공 시 모달 닫고 폼 리셋
+ form.reset()
+ onOpenChange(false)
+ onSuccess()
+ }
+
+ function handleDialogOpenChange(nextOpen: boolean) {
+ if (!nextOpen) {
+ form.reset()
+ }
+ onOpenChange(nextOpen)
+ }
+
+ if (!contact) return null
+
+ return (
+ <Dialog open={open} onOpenChange={handleDialogOpenChange}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>연락처 수정</DialogTitle>
+ <DialogDescription>
+ 연락처 정보를 수정하고 <b>Update</b> 버튼을 누르세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* shadcn/ui Form을 이용해 react-hook-form과 연결 */}
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)}>
+ <div className="space-y-4 py-4">
+ <FormField
+ control={form.control}
+ name="contactName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당자명</FormLabel>
+ <FormControl>
+ <Input placeholder="예: 홍길동" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactPosition"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>직급</FormLabel>
+ <FormControl>
+ <Input placeholder="예: 과장" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactDepartment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>부서</FormLabel>
+ <FormControl>
+ <Input placeholder="예: 영업부" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactTask"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당업무</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value || undefined}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="담당업무를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {taskOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactEmail"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>이메일</FormLabel>
+ <FormControl>
+ <Input placeholder="name@company.com" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contactPhone"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>전화번호</FormLabel>
+ <FormControl>
+ <Input placeholder="010-1234-5678" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <DialogFooter>
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+ 취소
+ </Button>
+ <Button type="submit" disabled={form.formState.isSubmitting}>
+ 수정
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/vendors/repository.ts b/lib/vendors/repository.ts
index d2be43ca..5b9b1116 100644
--- a/lib/vendors/repository.ts
+++ b/lib/vendors/repository.ts
@@ -175,6 +175,26 @@ export const getVendorContactsById = async (id: number): Promise<VendorContact |
return contact
};
+export const getVendorContactById = async (id: number): Promise<VendorContact | null> => {
+ const contactsRes = await db.select().from(vendorContacts).where(eq(vendorContacts.id, id)).execute();
+ if (contactsRes.length === 0) return null;
+
+ const contact = contactsRes[0];
+ return contact
+};
+
+export async function updateVendorContactById(
+ tx: PgTransaction<any, any, any>,
+ id: number,
+ data: Partial<VendorContact>
+) {
+ return tx
+ .update(vendorContacts)
+ .set(data)
+ .where(eq(vendorContacts.id, id))
+ .returning();
+}
+
export async function selectVendorContacts(
tx: PgTransaction<any, any, any>,
params: {
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index f4ba815c..e6a2a139 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -34,6 +34,8 @@ import {
countVendorMaterials,
selectVendorMaterials,
insertVendorMaterial,
+ getVendorContactById,
+ updateVendorContactById,
} from "./repository";
@@ -42,6 +44,7 @@ import type {
GetVendorsSchema,
GetVendorContactsSchema,
CreateVendorContactSchema,
+ UpdateVendorContactSchema,
GetVendorItemsSchema,
CreateVendorItemSchema,
GetRfqHistorySchema,
@@ -635,6 +638,8 @@ export async function createVendorContact(input: CreateVendorContactSchema) {
vendorId: input.vendorId,
contactName: input.contactName,
contactPosition: input.contactPosition || "",
+ contactDepartment: input.contactDepartment || "",
+ contactTask: input.contactTask || "",
contactEmail: input.contactEmail,
contactPhone: input.contactPhone || "",
isPrimary: input.isPrimary || false,
@@ -651,6 +656,35 @@ export async function createVendorContact(input: CreateVendorContactSchema) {
}
}
+export async function updateVendorContact(id: number, input: UpdateVendorContactSchema) {
+ unstable_noStore(); // Next.js 서버 액션 캐싱 방지
+ try {
+ const vendorContact = await getVendorContactById(id);
+ if (!vendorContact) {
+ return { data: null, error: "Contact not found" };
+ }
+
+ await db.transaction(async (tx) => {
+ // DB Update
+ await updateVendorContactById(tx, id, {
+ contactName: input.contactName,
+ contactPosition: input.contactPosition,
+ contactDepartment: input.contactDepartment,
+ contactTask: input.contactTask,
+ contactEmail: input.contactEmail,
+ contactPhone: input.contactPhone,
+ isPrimary: input.isPrimary,
+ });
+ });
+
+ // 캐시 무효화 (협력업체 연락처 목록 등)
+ revalidateTag(`vendor-contacts-${vendorContact.vendorId}`);
+
+ return { data: null, error: null };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
///item
diff --git a/lib/vendors/validations.ts b/lib/vendors/validations.ts
index 44237963..88a39651 100644
--- a/lib/vendors/validations.ts
+++ b/lib/vendors/validations.ts
@@ -334,22 +334,26 @@ export const createVendorSchema = z
export const createVendorContactSchema = z.object({
vendorId: z.number(),
contactName: z.string()
- .min(1, "Contact name is required")
- .max(255, "Max length 255"), // 신규 생성 시 반드시 입력
- contactPosition: z.string().max(100, "Max length 100"),
- contactEmail: z.string().email(),
- contactPhone: z.string().max(50, "Max length 50").optional(),
+ .min(1, "담당자명은 필수 입력사항입니다.")
+ .max(255, "최대 255자까지 입력 가능합니다."), // 신규 생성 시 반드시 입력
+ contactPosition: z.string().max(100, "최대 100자까지 입력 가능합니다."),
+ contactDepartment: z.string().max(100, "최대 100자까지 입력 가능합니다."),
+ contactTask: z.string().max(100, "최대 100자까지 입력 가능합니다."),
+ contactEmail: z.string().email("올바른 이메일 형식이 아닙니다."),
+ contactPhone: z.string().max(50, "최대 50자까지 입력 가능합니다.").optional(),
isPrimary: z.boolean(),
});
export const updateVendorContactSchema = z.object({
contactName: z.string()
- .min(1, "Contact name is required")
- .max(255, "Max length 255"), // 신규 생성 시 반드시 입력
- contactPosition: z.string().max(100, "Max length 100").optional(),
- contactEmail: z.string().email().optional(),
- contactPhone: z.string().max(50, "Max length 50").optional(),
+ .min(1, "담당자명은 필수 입력사항입니다.")
+ .max(255, "최대 255자까지 입력 가능합니다."), // 신규 생성 시 반드시 입력
+ contactPosition: z.string().max(100, "최대 100자까지 입력 가능합니다.").optional(),
+ contactDepartment: z.string().max(100, "최대 100자까지 입력 가능합니다.").optional(),
+ contactTask: z.string().max(100, "최대 100자까지 입력 가능합니다.").optional(),
+ contactEmail: z.string().email("올바른 이메일 형식이 아닙니다.").optional(),
+ contactPhone: z.string().max(50, "최대 50자까지 입력 가능합니다.").optional(),
isPrimary: z.boolean().optional(),
});