diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/po/vendor-table/service.ts | 12 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-columns.tsx | 4 | ||||
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 44 | ||||
| -rw-r--r-- | lib/users/department-domain/service.ts | 161 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-comment-dialog.tsx | 183 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-stage-toolbar.tsx | 2 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-stages-columns.tsx | 25 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-stages-service.ts | 53 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/document-stages-table.tsx | 17 | ||||
| -rw-r--r-- | lib/vendor-document-list/plant/shi-buyer-system-api.ts | 3 |
10 files changed, 416 insertions, 88 deletions
diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts index 88f6ddd5..195144a2 100644 --- a/lib/po/vendor-table/service.ts +++ b/lib/po/vendor-table/service.ts @@ -301,17 +301,17 @@ export async function handleVendorPOAction( switch (action) { case "pcr-create": - return { success: true, message: "PCR이 성공적으로 생성되었습니다." }; + return { success: true, message: "개발중" }; case "approve": - return { success: true, message: "계약이 승인되었습니다." }; + return { success: true, message: '개발중' }; case "cancel-approve": - return { success: true, message: "승인이 취소되었습니다." }; + return { success: true, message: '개발중' }; case "reject-contract": - return { success: true, message: "계약이 거절되었습니다." }; + return { success: true, message: '개발중' }; case "print-contract": - return { success: true, message: "계약서 출력이 요청되었습니다." }; + return { success: true, message: '개발중' }; default: - return { success: false, message: "알 수 없는 액션입니다." }; + return { success: false, message: '개발중' }; } } catch (err) { console.error("Error in handleVendorPOAction:", err); diff --git a/lib/po/vendor-table/vendor-po-columns.tsx b/lib/po/vendor-table/vendor-po-columns.tsx index 1a655b0c..0910eaf8 100644 --- a/lib/po/vendor-table/vendor-po-columns.tsx +++ b/lib/po/vendor-table/vendor-po-columns.tsx @@ -425,7 +425,7 @@ export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect </Tooltip> </TooltipProvider> - {/* 드롭다운 메뉴 */} + {/* 드롭다운 메뉴 <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> @@ -500,7 +500,7 @@ export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect 연동표입력 </DropdownMenuItem> </DropdownMenuContent> - </DropdownMenu> + </DropdownMenu> */} </div> ); }, diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts index bb80c454..e3ab1102 100644 --- a/lib/shi-api/shi-api-utils.ts +++ b/lib/shi-api/shi-api-utils.ts @@ -2,9 +2,10 @@ import { nonsapUser, users } from '@/db/schema'; import db from '@/db/db'; +import { eq } from 'drizzle-orm'; import { debugError, debugLog, debugWarn, debugSuccess } from '@/lib/debug-utils'; import { bulkUpsert } from '@/lib/soap/batch-utils'; -import { autoAssignPendingUsersDomains } from '@/lib/users/department-domain/service'; +import { autoAssignUsersDomains, type UserDomain } from '@/lib/users/department-domain/service'; const shiApiBaseUrl = process.env.SHI_API_BASE_URL; const shiNonsapUserSegment = process.env.SHI_NONSAP_USER_SEGMENT; @@ -91,11 +92,34 @@ export const getAllNonsapUser = async () => { for (let i = 0; i < sourceData.length; i += CHUNK_SIZE) { const chunk = sourceData.slice(i, i + CHUNK_SIZE); debugLog(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Processing ${chunk.length} users (${i + 1}-${Math.min(i + chunk.length, sourceData.length)}/${sourceData.length})`); - + try { + // 청크 내 이메일별 기존 domain 정보 조회를 위한 Map 생성 + const existingDomainMap = new Map<string, string>(); + + // 각 사용자에 대해 개별적으로 기존 domain 조회 (메모리 효율성 위해) + for (const u of chunk) { + if (u.EMAIL_ADR) { + try { + const existingUser = await db + .select({ domain: users.domain }) + .from(users) + .where(eq(users.email, u.EMAIL_ADR)) + .limit(1); + + if (existingUser.length > 0) { + existingDomainMap.set(u.EMAIL_ADR.toLowerCase(), existingUser[0].domain); + } + } catch (error) { + // 조회 실패 시 무시하고 계속 진행 + console.warn(`Failed to lookup existing user for email ${u.EMAIL_ADR}:`, error); + } + } + } + // 청크 단위로 매핑 수행 const mappedChunk: InsertUser[] = []; - + for (const u of chunk) { const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부 const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부 @@ -104,18 +128,24 @@ export const getAllNonsapUser = async () => { // S = 정직원 const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S'; - const mappedUser: Partial<InsertUser> = { + // 기존 사용자인지 확인하여 domain 결정 + const email = u.EMAIL_ADR; + const existingDomain = email ? existingDomainMap.get(email.toLowerCase()) as UserDomain | undefined : undefined; + // 기존 사용자(existingDomain !== undefined)는 기존 domain을 무조건 유지, 새 사용자는 pending + const domain = existingDomain !== undefined ? existingDomain : 'pending'; + + const mappedUser: Partial<InsertUser> = { // mapped fields nonsapUserId: u.USR_ID || undefined, employeeNumber: u.EMPNO || undefined, knoxId: u.MYSNG_ID || undefined, name: u.USR_NM || undefined, - email: u.EMAIL_ADR || undefined, + email: email || undefined, epId: u.MYSNG_USR_ID || undefined, deptCode: u.DEPTCD || undefined, deptName: u.DEPTNM || undefined, phone: u.HP_NO || undefined, - domain: 'pending', // SHI-API를 통해 동기화되는 사용자는 pending 도메인으로 설정 + domain, // 기존 사용자는 기존 domain 유지, 새 사용자는 pending isAbsent, isDeletedOnNonSap: isDeleted, isActive, @@ -160,7 +190,7 @@ export const getAllNonsapUser = async () => { // ** 4. 사용자 동기화 완료 후 부서별 도메인 자동 할당 처리 ** debugLog('[DOMAIN-AUTO-ASSIGN] SHI-API 동기화 완료 후 부서별 도메인 자동 할당 시작'); try { - const domainAssignResult = await autoAssignPendingUsersDomains(); + const domainAssignResult = await autoAssignUsersDomains(); debugSuccess(`[DOMAIN-AUTO-ASSIGN] 부서별 도메인 자동 할당 완료: ${domainAssignResult.message}`); } catch (domainError) { debugError('[DOMAIN-AUTO-ASSIGN] 부서별 도메인 자동 할당 실패', domainError); diff --git a/lib/users/department-domain/service.ts b/lib/users/department-domain/service.ts index 834de83a..6be6704c 100644 --- a/lib/users/department-domain/service.ts +++ b/lib/users/department-domain/service.ts @@ -8,7 +8,7 @@ import { departmentDomainAssignmentHistory } from "@/db/schema/departmentDomainAssignments"; import { users } from "@/db/schema/users"; -import { and, eq, inArray, desc } from "drizzle-orm"; +import { and, eq, inArray, desc, sql, ne } from "drizzle-orm"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { getErrorMessage } from "@/lib/handle-error"; @@ -439,98 +439,125 @@ export async function getDepartmentDomainStats() { )(); } -// pending 도메인 사용자들의 부서별 도메인 자동 할당 -export async function autoAssignPendingUsersDomains() { +// 도메인 사용자들의 부서별 도메인 자동 할당 +export async function autoAssignUsersDomains() { unstable_noStore(); try { - console.log('[DOMAIN-AUTO-ASSIGN] pending 사용자 도메인 자동 할당 시작'); - - // 1. pending 도메인 사용자들의 부서 정보 조회 - const pendingUsers = await db + console.log('[DOMAIN-AUTO-ASSIGN] 사용자 도메인 자동 할당 시작'); + + // 1. 모든 활성 부서별 도메인 할당 정보 조회 + const domainAssignments = await db .select({ - id: users.id, - email: users.email, - name: users.name, - deptCode: users.deptCode, - deptName: users.deptName, + companyCode: departmentDomainAssignments.companyCode, + departmentCode: departmentDomainAssignments.departmentCode, + assignedDomain: departmentDomainAssignments.assignedDomain, }) - .from(users) - .where(eq(users.domain, 'pending')); + .from(departmentDomainAssignments) + .where(eq(departmentDomainAssignments.isActive, true)); - console.log(`[DOMAIN-AUTO-ASSIGN] pending 사용자 ${pendingUsers.length}명 발견`); + console.log(`[DOMAIN-AUTO-ASSIGN] ${domainAssignments.length}개 부서의 도메인 할당 정보 로드됨`); - if (pendingUsers.length === 0) { - console.log('[DOMAIN-AUTO-ASSIGN] pending 도메인 사용자가 없습니다.'); + if (domainAssignments.length === 0) { + console.log('[DOMAIN-AUTO-ASSIGN] 활성 부서별 도메인 할당 정보가 없습니다.'); return { success: true, processedCount: 0, assignedCount: 0, skippedCount: 0, - message: 'pending 도메인 사용자가 없습니다.', + message: '활성 부서별 도메인 할당 정보가 없습니다.', }; } - // 2. 모든 활성 부서별 도메인 할당 정보 조회 - const domainAssignments = await db - .select({ - companyCode: departmentDomainAssignments.companyCode, - departmentCode: departmentDomainAssignments.departmentCode, - assignedDomain: departmentDomainAssignments.assignedDomain, - }) - .from(departmentDomainAssignments) - .where(eq(departmentDomainAssignments.isActive, true)); + let totalAssignedCount = 0; + let totalSkippedCount = 0; - // 3. 부서별 도메인 매핑을 위한 Map 생성 - const domainMap = new Map<string, string>(); - domainAssignments.forEach(assignment => { - const key = `${assignment.companyCode}-${assignment.departmentCode}`; - domainMap.set(key, assignment.assignedDomain); - }); - - console.log(`[DOMAIN-AUTO-ASSIGN] ${domainAssignments.length}개 부서의 도메인 할당 정보 로드됨`); + // 2. 각 부서별로 partners가 아닌 도메인 사용자들을 업데이트 + for (const assignment of domainAssignments) { + try { + // 해당 부서의 partners가 아닌 도메인 사용자들 중 도메인이 다른 사용자만 업데이트 + const updateResult = await db + .update(users) + .set({ + domain: assignment.assignedDomain as UserDomain, + updatedAt: new Date(), + }) + .where(and( + ne(users.domain, 'partners'), // partners가 아닌 모든 도메인 사용자 + ne(users.domain, assignment.assignedDomain), // 이미 할당된 도메인과 다른 경우만 + eq(users.deptCode, assignment.departmentCode) // 해당 부서의 사용자 + )); - let assignedCount = 0; - let skippedCount = 0; + const affectedRows = updateResult.rowCount || 0; - // 4. 각 pending 사용자에 대해 도메인 할당 처리 - for (const user of pendingUsers) { - try { - // 사용자의 부서에 대한 도메인 할당 정보 확인 - // 현재 회사 코드 사용 (환경변수 또는 기본값) - const companyCode = process.env.CURRENT_COMPANY_CODE || 'D60'; - const domainKey = `${companyCode}-${user.deptCode || ''}`; - const assignedDomain = domainMap.get(domainKey); - - if (assignedDomain && assignedDomain !== 'pending') { - // 도메인 할당 정보가 있으면 업데이트 - await db - .update(users) - .set({ - domain: assignedDomain as UserDomain, - updatedAt: new Date(), - }) - .where(eq(users.id, user.id)); - - assignedCount++; - console.log(`[DOMAIN-AUTO-ASSIGN] 사용자 ${user.name}(${user.email}) -> ${assignedDomain} 도메인 할당`); + if (affectedRows > 0) { + totalAssignedCount += affectedRows; + console.log(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} -> ${assignment.assignedDomain} 도메인으로 ${affectedRows}명 업데이트`); } else { - // 할당 정보가 없으면 pending 유지 - skippedCount++; - console.log(`[DOMAIN-AUTO-ASSIGN] 사용자 ${user.name}(${user.email}) -> 부서(${user.deptCode})에 대한 도메인 할당 정보 없음, pending 유지`); + totalSkippedCount++; + console.log(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} -> 업데이트할 사용자가 없음`); } + } catch (error) { - console.error(`[DOMAIN-AUTO-ASSIGN] 사용자 ${user.email} 처리 실패:`, error); - skippedCount++; + console.error(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} 처리 실패:`, error); + totalSkippedCount++; } } + // 3. partners가 아닌 도메인을 가진 총 사용자 수 확인 + const totalNonPartnersUsers = await db + .select({ count: sql<number>`count(*)` }) + .from(users) + .where(ne(users.domain, 'partners')); + + const totalNonPartnersCount = totalNonPartnersUsers[0]?.count || 0; + + // 4. 부서 코드가 없어서 pending으로 남아있는 사용자 조회 및 로깅 + const pendingUsersWithoutDept = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + employeeNumber: users.employeeNumber, + deptCode: users.deptCode, + deptName: users.deptName, + }) + .from(users) + .where(and( + eq(users.domain, 'pending'), + ne(users.domain, 'partners') + )); + + // 부서 코드가 없는 pending 사용자 필터링 및 로깅 + const usersWithoutDeptCode = pendingUsersWithoutDept.filter(u => !u.deptCode); + + if (usersWithoutDeptCode.length > 0) { + console.warn(`[DOMAIN-AUTO-ASSIGN] ⚠️ 부서 코드가 없어 pending 상태로 남은 사용자: ${usersWithoutDeptCode.length}명`); + console.warn(`[DOMAIN-AUTO-ASSIGN] 부서 코드 없는 사용자 목록:`); + usersWithoutDeptCode.forEach(user => { + console.warn(` - ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}, 사번: ${user.employeeNumber || 'N/A'}, 부서코드: ${user.deptCode || 'NULL'}, 부서명: ${user.deptName || 'N/A'}`); + }); + } + + // 부서가 있지만 할당 규칙이 없어서 pending인 사용자도 로깅 + const usersWithDeptButNoAssignment = pendingUsersWithoutDept.filter(u => !!u.deptCode); + + if (usersWithDeptButNoAssignment.length > 0) { + console.info(`[DOMAIN-AUTO-ASSIGN] ℹ️ 부서는 있지만 도메인 할당 규칙이 없어 pending 상태인 사용자: ${usersWithDeptButNoAssignment.length}명`); + console.info(`[DOMAIN-AUTO-ASSIGN] 할당 규칙 없는 부서의 사용자 목록:`); + usersWithDeptButNoAssignment.forEach(user => { + console.info(` - ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}, 부서코드: ${user.deptCode}, 부서명: ${user.deptName || 'N/A'}`); + }); + } + const result = { success: true, - processedCount: pendingUsers.length, - assignedCount, - skippedCount, - message: `${assignedCount}명의 사용자에게 도메인이 자동 할당되었습니다. (스킵: ${skippedCount}명)`, + processedCount: totalNonPartnersCount, + assignedCount: totalAssignedCount, + skippedCount: totalSkippedCount, + pendingWithoutDeptCount: usersWithoutDeptCode.length, + pendingWithDeptButNoAssignmentCount: usersWithDeptButNoAssignment.length, + message: `${totalAssignedCount}명의 사용자에게 도메인이 자동 할당되었습니다.`, }; console.log(`[DOMAIN-AUTO-ASSIGN] 완료: ${JSON.stringify(result)}`); diff --git a/lib/vendor-document-list/plant/document-comment-dialog.tsx b/lib/vendor-document-list/plant/document-comment-dialog.tsx new file mode 100644 index 00000000..3dc3d321 --- /dev/null +++ b/lib/vendor-document-list/plant/document-comment-dialog.tsx @@ -0,0 +1,183 @@ +"use client" + +import React from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Loader2, MessageSquare } from "lucide-react" +import { toast } from "sonner" +import { useSession } from "next-auth/react" +import { addDocumentComment } from "./document-stages-service" +import { useRouter } from "next/navigation" +import { cn } from "@/lib/utils" + +interface DocumentCommentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + documentId: number + docNumber: string + currentComment: string | null +} + +export function DocumentCommentDialog({ + open, + onOpenChange, + documentId, + docNumber, + currentComment, +}: DocumentCommentDialogProps) { + const [newComment, setNewComment] = React.useState("") + const [isSubmitting, setIsSubmitting] = React.useState(false) + const { data: session } = useSession() + const router = useRouter() + + // 기존 코멘트를 줄 단위로 파싱 + const existingComments = React.useMemo(() => { + if (!currentComment) return [] + return currentComment.split('\n').filter(line => line.trim() !== '') + }, [currentComment]) + + const handleSubmit = async () => { + if (!newComment.trim()) { + toast.error("코멘트 내용을 입력해주세요.") + return + } + + if (!session?.user?.name || !session?.user?.email) { + toast.error("사용자 정보를 가져올 수 없습니다.") + return + } + + setIsSubmitting(true) + try { + const result = await addDocumentComment({ + documentId, + newComment: newComment.trim(), + userInfo: { + name: session.user.name, + email: session.user.email, + }, + }) + + if (result.success) { + toast.success("코멘트가 추가되었습니다.") + setNewComment("") + onOpenChange(false) + router.refresh() + } else { + toast.error(result.error || "코멘트 추가 중 오류가 발생했습니다.") + } + } catch (error) { + console.error("Error adding comment:", error) + toast.error("코멘트 추가 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + } + } + + const handleClose = () => { + if (!isSubmitting) { + setNewComment("") + onOpenChange(false) + } + } + + return ( + <Dialog open={open} onOpenChange={handleClose}> + <DialogContent className="sm:max-w-[600px] max-h-[80vh] flex flex-col"> + <DialogHeader className="flex-shrink-0"> + <DialogTitle className="flex items-center gap-2"> + <MessageSquare className="h-5 w-5" /> + Document Comment + </DialogTitle> + <DialogDescription> + 문서번호: <span className="font-mono font-medium">{docNumber}</span> + </DialogDescription> + </DialogHeader> + + <div className="flex-1 flex flex-col gap-4 min-h-0"> + {/* 기존 코멘트 표시 영역 (읽기 전용) */} + <div className="space-y-2"> + <Label className="text-sm font-medium">기존 코멘트</Label> + <ScrollArea className="h-[200px] w-full rounded-md border p-4 bg-gray-50 dark:bg-gray-900"> + {existingComments.length > 0 ? ( + <div className="space-y-2"> + {existingComments.map((comment, index) => ( + <div + key={index} + className={cn( + "text-sm p-2 rounded", + index % 2 === 0 + ? "bg-white dark:bg-gray-800" + : "bg-gray-100 dark:bg-gray-850" + )} + > + {comment} + </div> + ))} + </div> + ) : ( + <div className="flex items-center justify-center h-full text-sm text-muted-foreground"> + 기존 코멘트가 없습니다. + </div> + )} + </ScrollArea> + </div> + + {/* 새 코멘트 입력 영역 */} + <div className="space-y-2"> + <Label htmlFor="new-comment" className="text-sm font-medium"> + 새 코멘트 추가 + </Label> + <Textarea + id="new-comment" + value={newComment} + onChange={(e) => setNewComment(e.target.value)} + placeholder="새로운 코멘트를 입력하세요..." + rows={4} + className="resize-none" + disabled={isSubmitting} + /> + <p className="text-xs text-muted-foreground"> + 작성자 정보와 함께 코멘트가 추가됩니다: [{session?.user?.name}·{session?.user?.email}] + </p> + </div> + </div> + + <DialogFooter className="flex-shrink-0"> + <Button + variant="outline" + onClick={handleClose} + disabled={isSubmitting} + > + 닫기 + </Button> + <Button + onClick={handleSubmit} + disabled={isSubmitting || !newComment.trim()} + > + {isSubmitting ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 추가 중... + </> + ) : ( + "코멘트 추가" + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} + + diff --git a/lib/vendor-document-list/plant/document-stage-toolbar.tsx b/lib/vendor-document-list/plant/document-stage-toolbar.tsx index f676e1fc..51767528 100644 --- a/lib/vendor-document-list/plant/document-stage-toolbar.tsx +++ b/lib/vendor-document-list/plant/document-stage-toolbar.tsx @@ -103,7 +103,7 @@ export function DocumentsTableToolbarActions({ {(() => { const selectedRows = table.getFilteredSelectedRowModel().rows; const deletableDocuments = selectedRows - .map((row) => row.original)s + .map((row) => row.original) .filter((doc) => !doc.buyerSystemStatus); // buyerSystemStatus가 null인 것만 필터링 return deletableDocuments.length > 0 ? ( diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx index af68ddb2..d5dfc895 100644 --- a/lib/vendor-document-list/plant/document-stages-columns.tsx +++ b/lib/vendor-document-list/plant/document-stages-columns.tsx @@ -380,14 +380,31 @@ export function getDocumentStagesColumns({ ), cell: ({ row }) => { const doc = row.original + const hasComment = doc.buyerSystemComment && doc.buyerSystemComment.trim() !== '' return ( - <div className="flex items-center gap-2"> - {doc.buyerSystemComment} - </div> + <Button + variant="ghost" + size="sm" + className="h-8 gap-2" + onClick={(e) => { + e.stopPropagation() + setRowAction({ row, type: "view_comment" }) + }} + > + <MessageSquare className={cn( + "h-4 w-4", + hasComment ? "text-blue-600 dark:text-blue-400" : "text-gray-400" + )} /> + {hasComment ? ( + <span className="text-xs">View ({doc.buyerSystemComment.split('\n').length})</span> + ) : ( + <span className="text-xs text-muted-foreground">Add</span> + )} + </Button> ) }, - size: 180, + size: 150, enableResizing: true, meta: { excelHeader: "Document Comment" diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts index 5f803104..cff448d5 100644 --- a/lib/vendor-document-list/plant/document-stages-service.ts +++ b/lib/vendor-document-list/plant/document-stages-service.ts @@ -1201,6 +1201,59 @@ export async function pullDocumentStatusFromSHI( } } +// 문서 코멘트 추가 +export async function addDocumentComment(input: { + documentId: number + newComment: string + userInfo: { + name: string + email: string + } +}) { + try { + // 1. 현재 문서 정보 조회 + const document = await db.query.stageDocuments.findFirst({ + where: eq(stageDocuments.id, input.documentId), + }) + + if (!document) { + return { success: false, error: "문서를 찾을 수 없습니다." } + } + + // 2. 새 코멘트 포맷팅: [이름·이메일] 코멘트내용 + const timestamp = new Date().toISOString().split('T')[0] // YYYY-MM-DD + const formattedComment = `[${input.userInfo.name}·${input.userInfo.email}] ${timestamp}: ${input.newComment}` + + // 3. 기존 코멘트와 결합 + const updatedComment = document.buyerSystemComment + ? `${document.buyerSystemComment}\n${formattedComment}` + : formattedComment + + // 4. 문서 업데이트 + await db + .update(stageDocuments) + .set({ + buyerSystemComment: updatedComment, + updatedAt: new Date(), + }) + .where(eq(stageDocuments.id, input.documentId)) + + // 5. 캐시 무효화 + revalidatePath(`/partners/document-list-only/${document.contractId}`) + + return { + success: true, + data: { comment: updatedComment } + } + } catch (error) { + console.error("코멘트 추가 실패:", error) + return { + success: false, + error: "코멘트 추가 중 오류가 발생했습니다." + } + } +} + interface FileValidation { projectId: number diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx index 6cc112e3..63f0eae6 100644 --- a/lib/vendor-document-list/plant/document-stages-table.tsx +++ b/lib/vendor-document-list/plant/document-stages-table.tsx @@ -34,6 +34,7 @@ import { EditDocumentDialog } from "./document-stage-dialogs" import { EditStageDialog } from "./document-stage-dialogs" import { DocumentsTableToolbarActions } from "./document-stage-toolbar" import { useSession } from "next-auth/react" +import { DocumentCommentDialog } from "./document-comment-dialog" interface DocumentStagesTableProps { promises: Promise<[Awaited<ReturnType<typeof getDocumentStagesOnly>>]> @@ -68,6 +69,7 @@ export function DocumentStagesTable({ const [editDocumentOpen, setEditDocumentOpen] = React.useState(false) const [editStageOpen, setEditStageOpen] = React.useState(false) const [excelImportOpen, setExcelImportOpen] = React.useState(false) + const [commentDialogOpen, setCommentDialogOpen] = React.useState(false) // 선택된 항목들 const [selectedDocument, setSelectedDocument] = React.useState<StageDocumentsView | null>(null) @@ -101,6 +103,9 @@ export function DocumentStagesTable({ } setExpandedRows(newExpanded) break + case "view_comment": + setCommentDialogOpen(true) + break } } }, @@ -164,6 +169,7 @@ export function DocumentStagesTable({ setEditDocumentOpen(false) setEditStageOpen(false) setExcelImportOpen(false) + setCommentDialogOpen(false) setSelectedDocument(null) setSelectedStageId(null) setRowAction(null) @@ -413,6 +419,17 @@ export function DocumentStagesTable({ documents={rowAction?.row.original ? [rowAction?.row.original] : []} onSuccess={() => rowAction?.row.toggleSelected(false)} /> + + <DocumentCommentDialog + open={commentDialogOpen} + onOpenChange={(open) => { + if (!open) closeAllDialogs() + else setCommentDialogOpen(open) + }} + documentId={selectedDocument?.documentId || 0} + docNumber={selectedDocument?.docNumber || ''} + currentComment={selectedDocument?.buyerSystemComment || null} + /> </div> ) }
\ No newline at end of file diff --git a/lib/vendor-document-list/plant/shi-buyer-system-api.ts b/lib/vendor-document-list/plant/shi-buyer-system-api.ts index b0462af8..9256eaf4 100644 --- a/lib/vendor-document-list/plant/shi-buyer-system-api.ts +++ b/lib/vendor-document-list/plant/shi-buyer-system-api.ts @@ -246,6 +246,7 @@ export class ShiBuyerSystemAPI { vendorDocNumber: stageDocuments.vendorDocNumber, title: stageDocuments.title, status: stageDocuments.status, + buyerSystemComment: stageDocuments.buyerSystemComment, // 코멘트 필드 추가 projectCode: sql<string>`(SELECT code FROM projects WHERE id = ${stageDocuments.projectId})`, vendorCode: sql<string>`(SELECT vendor_code FROM vendors WHERE id = ${stageDocuments.vendorId})`, vendorName: sql<string>`(SELECT vendor_name FROM vendors WHERE id = ${stageDocuments.vendorId})`, @@ -300,7 +301,7 @@ export class ShiBuyerSystemAPI { OWN_DOC_NO: doc.vendorDocNumber || doc.docNumber, DSC: doc.title, DOC_CLASS: 'B3', - COMMENT: '', + COMMENT: doc.buyerSystemComment || '', // 실제 코멘트 전송 // 조민정 프로 요청으로 'ACTIVE' --> '생성요청' 값으로 변경 (251002,김준회) STATUS: '생성요청', CRTER: 'EVCP_SYSTEM', |
