summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-03 13:54:38 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-03 13:54:38 +0900
commit8945be5ea89365f8a686a0e65b5a7d5b61c2ca20 (patch)
treed7ee4acd93bcffacea3c095cb60d5a9c67998be9 /lib
parentdefda07c0bb4b0bd444ca8dc4fd3f89322bda0ce (diff)
(김준회) 부서별 권한관리, swp 코멘트 기능, 벤더 po, shi-api 동기화 로직 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/po/vendor-table/service.ts12
-rw-r--r--lib/po/vendor-table/vendor-po-columns.tsx4
-rw-r--r--lib/shi-api/shi-api-utils.ts44
-rw-r--r--lib/users/department-domain/service.ts161
-rw-r--r--lib/vendor-document-list/plant/document-comment-dialog.tsx183
-rw-r--r--lib/vendor-document-list/plant/document-stage-toolbar.tsx2
-rw-r--r--lib/vendor-document-list/plant/document-stages-columns.tsx25
-rw-r--r--lib/vendor-document-list/plant/document-stages-service.ts53
-rw-r--r--lib/vendor-document-list/plant/document-stages-table.tsx17
-rw-r--r--lib/vendor-document-list/plant/shi-buyer-system-api.ts3
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',