summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx15
-rw-r--r--app/api/files/[...path]/route.ts3
-rw-r--r--components/additional-info/join-form.tsx12
-rw-r--r--components/information/information-button.tsx113
-rw-r--r--components/notice/notice-client.tsx116
-rw-r--r--components/notice/notice-create-dialog.tsx2
-rw-r--r--components/notice/notice-edit-sheet.tsx2
-rw-r--r--components/vendor-info/pq-simple-dialog.tsx5
-rw-r--r--components/vendor-regular-registrations/document-status-dialog.tsx203
-rw-r--r--config/vendorRegularRegistrationsColumnsConfig.ts2
-rw-r--r--lib/information/service.ts74
-rw-r--r--lib/information/table/update-information-dialog.tsx4
-rw-r--r--lib/notice/repository.ts155
-rw-r--r--lib/notice/service.ts215
-rw-r--r--lib/notice/validations.ts57
-rw-r--r--lib/vendor-regular-registrations/repository.ts35
-rw-r--r--lib/vendor-regular-registrations/service.ts13
17 files changed, 447 insertions, 579 deletions
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
index 0e4dccf4..536bed95 100644
--- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
@@ -25,7 +25,7 @@ import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/
import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service";
import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service";
import { getVendorAttachmentsByType, getVendorPeriodicGrade, getVendorTypeInfo } from "@/lib/vendor-info/service";
-import { downloadFile } from "@/lib/file-download";
+// downloadFile은 동적으로 import
import {
Table,
TableBody,
@@ -440,6 +440,8 @@ export default function BasicInfoClient({
// DocumentStatusDialog가 기대하는 형태로 데이터 구성
const dialogData = {
// 기본 정보
+ id: result.data.registration?.id || 0,
+ vendorId: parseInt(vendorId),
companyName: result.data.vendor.vendorName,
businessNumber: result.data.vendor.taxId,
representative: result.data.vendor.representativeName,
@@ -449,6 +451,14 @@ export default function BasicInfoClient({
// 문서 제출 현황 - documentSubmissions 속성으로 매핑
documentSubmissions: result.data.documentStatus,
+ // 문서별 파일 정보 추가
+ documentFiles: result.data.documentFiles || {
+ businessRegistration: [],
+ creditEvaluation: [],
+ bankCopy: [],
+ auditResult: []
+ },
+
// 기본계약 정보
basicContracts: result.data.basicContracts || [],
@@ -507,6 +517,9 @@ export default function BasicInfoClient({
// 첨부파일 다운로드 핸들러
const handleAttachmentDownload = async (filePath: string, fileName: string) => {
try {
+ // 동적으로 downloadFile 함수 import
+ const { downloadFile } = await import('@/lib/file-download')
+
const result = await downloadFile(filePath, fileName);
if (result.success) {
toast.success(`${fileName} 파일이 다운로드되었습니다.`);
diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts
index 0bc2e22f..2b58ca43 100644
--- a/app/api/files/[...path]/route.ts
+++ b/app/api/files/[...path]/route.ts
@@ -49,7 +49,8 @@ const isAllowedPath = (requestedPath: string): boolean => {
'vendors/nda',
'vendors',
'pq',
- 'pq/vendor'
+ 'pq/vendor',
+ 'information'
];
return allowedPaths.some(allowed =>
diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx
index ca0c60d5..90effddb 100644
--- a/components/additional-info/join-form.tsx
+++ b/components/additional-info/join-form.tsx
@@ -87,7 +87,6 @@ import { InformationButton } from "@/components/information/information-button"
// 보안 파일 다운로드 유틸리티 import
import {
- downloadFile,
quickDownload,
smartFileAction,
getFileInfo,
@@ -366,6 +365,7 @@ export function InfoForm() {
const downloadUrl = `/api/vendors/attachments/download?id=${fileId}&vendorId=${Number(companyId)}`;
// 보안 다운로드 유틸리티 사용
+ const { downloadFile } = await import('@/lib/file-download')
const result = await downloadFile(downloadUrl, fileName, {
action: 'download',
showToast: false, // 우리가 직접 토스트 관리
@@ -413,6 +413,8 @@ export function InfoForm() {
const fileName = `vendor-${companyId}-files.zip`;
// 보안 다운로드 유틸리티 사용
+
+ const { downloadFile } = await import('@/lib/file-download')
const result = await downloadFile(downloadUrl, fileName, {
action: 'download',
showToast: false, // 우리가 직접 토스트 관리
@@ -708,7 +710,7 @@ export function InfoForm() {
}
// 보안 정보 가져오기 (선택적으로 사용자에게 표시)
- const securityInfo = getSecurityInfo();
+ // const securityInfo = getSecurityInfo();
// Render
return (
@@ -746,9 +748,9 @@ export function InfoForm() {
)}
{/* 보안 정보 표시 (선택적) */}
- <div className="text-xs text-muted-foreground">
+ {/* <div className="text-xs text-muted-foreground">
<p>📁 허용 파일 크기: {securityInfo.maxFileSizeFormatted} | 남은 다운로드: {securityInfo.remainingDownloads}/분</p>
- </div>
+ </div> */}
</div>
<Separator />
@@ -1860,7 +1862,7 @@ export function InfoForm() {
domesticCredit: "not_submitted",
},
basicContracts: registrationData.basicContracts || [],
- documentFiles: {
+ documentFiles: registrationData.documentFiles || {
businessRegistration: [],
creditEvaluation: [],
bankCopy: [],
diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx
index 52079767..17f10502 100644
--- a/components/information/information-button.tsx
+++ b/components/information/information-button.tsx
@@ -12,14 +12,15 @@ import {
DialogTrigger,
} from "@/components/ui/dialog"
import { Info, Download, Edit, Loader2 } from "lucide-react"
-import { getCachedPageInformation, getCachedEditPermission } from "@/lib/information/service"
-import { getCachedPageNotices } from "@/lib/notice/service"
+import { getPageInformationDirect, getEditPermissionDirect } from "@/lib/information/service"
+import { getPageNotices } from "@/lib/notice/service"
import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog"
import { NoticeViewDialog } from "@/components/notice/notice-view-dialog"
-import type { PageInformation } from "@/db/schema/information"
+import type { PageInformation, InformationAttachment } from "@/db/schema/information"
import type { Notice } from "@/db/schema/notice"
import { useSession } from "next-auth/react"
import { formatDate } from "@/lib/utils"
+// downloadFile은 동적으로 import
interface InformationButtonProps {
pagePath: string
@@ -41,7 +42,7 @@ export function InformationButton({
}: InformationButtonProps) {
const { data: session } = useSession()
const [isOpen, setIsOpen] = useState(false)
- const [information, setInformation] = useState<PageInformation | null>(null)
+ const [information, setInformation] = useState<(PageInformation & { attachments: InformationAttachment[] }) | null>(null)
const [notices, setNotices] = useState<NoticeWithAuthor[]>([])
const [hasEditPermission, setHasEditPermission] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
@@ -59,19 +60,35 @@ export function InformationButton({
// pagePath 정규화 (앞의 / 제거)
const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath
+ console.log('🔍 Information Button - 데이터 로딩:', {
+ originalPath: pagePath,
+ normalizedPath: normalizedPath,
+ sessionUserId: session?.user?.id
+ })
+
// 병렬로 데이터 조회
const [infoResult, noticesResult] = await Promise.all([
- getCachedPageInformation(normalizedPath),
- getCachedPageNotices(normalizedPath)
+ getPageInformationDirect(normalizedPath),
+ getPageNotices(normalizedPath)
])
+ console.log('📊 조회 결과:', {
+ infoResult: infoResult ? {
+ id: infoResult.id,
+ pagePath: infoResult.pagePath,
+ pageName: infoResult.pageName,
+ attachmentsCount: infoResult.attachments?.length || 0
+ } : null,
+ noticesCount: noticesResult.length
+ })
+
setInformation(infoResult)
setNotices(noticesResult)
setDataLoaded(true)
// 권한 확인
if (session?.user?.id) {
- const hasPermission = await getCachedEditPermission(normalizedPath, session.user.id)
+ const hasPermission = await getEditPermissionDirect(normalizedPath, session.user.id)
setHasEditPermission(hasPermission)
}
} catch (error) {
@@ -109,16 +126,22 @@ export function InformationButton({
}
// 파일 다운로드 핸들러
- const handleDownload = () => {
- if (information?.attachmentFilePath) {
- // window.open 대신 link 요소 사용
- const link = document.createElement('a')
- link.href = information.attachmentFilePath
- link.target = '_blank'
- link.rel = 'noopener noreferrer'
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
+ const handleDownload = async (attachment: InformationAttachment) => {
+ try {
+ // 동적으로 downloadFile 함수 import
+ const { downloadFile } = await import('@/lib/file-download')
+
+ await downloadFile(
+ attachment.filePath,
+ attachment.fileName,
+ {
+ action: 'download',
+ showToast: true,
+ showSuccessToast: true
+ }
+ )
+ } catch (error) {
+ console.error('파일 다운로드 실패:', error)
}
}
@@ -145,7 +168,7 @@ export function InformationButton({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div>
- <DialogTitle>{information?.pageName}</DialogTitle>
+ <DialogTitle></DialogTitle>
</div>
</div>
</div>
@@ -231,29 +254,41 @@ export function InformationButton({
{/* 첨부파일 */}
<div className="space-y-3">
- <h4 className="font-semibold">첨부파일</h4>
+ <div className="flex items-center justify-between">
+ <h4 className="font-semibold">첨부파일</h4>
+ {information?.attachments && information.attachments.length > 0 && (
+ <span className="text-xs text-gray-500">{information.attachments.length}개</span>
+ )}
+ </div>
<div className="bg-gray-50 border rounded-lg p-4">
- {information?.attachmentFileName ? (
- <div className="flex items-center justify-between p-3 bg-white rounded border">
- <div className="flex-1">
- <div className="text-sm font-medium">
- {information.attachmentFileName}
- </div>
- {information.attachmentFileSize && (
- <div className="text-xs text-gray-500 mt-1">
- {information.attachmentFileSize}
+ {information?.attachments && information.attachments.length > 0 ? (
+ <div className="space-y-3">
+ {information.attachments.map((attachment) => (
+ <div
+ key={attachment.id}
+ className="flex items-center justify-between p-3 bg-white rounded border"
+ >
+ <div className="flex-1">
+ <div className="text-sm font-medium">
+ {attachment.fileName}
+ </div>
+ {attachment.fileSize && (
+ <div className="text-xs text-gray-500 mt-1">
+ {attachment.fileSize}
+ </div>
+ )}
</div>
- )}
- </div>
- <Button
- size="sm"
- variant="outline"
- onClick={handleDownload}
- className="flex items-center gap-1"
- >
- <Download className="h-3 w-3" />
- 다운로드
- </Button>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleDownload(attachment)}
+ className="flex items-center gap-1"
+ >
+ <Download className="h-3 w-3" />
+ 다운로드
+ </Button>
+ </div>
+ ))}
</div>
) : (
<div className="text-center text-gray-500">
diff --git a/components/notice/notice-client.tsx b/components/notice/notice-client.tsx
index e5c05d84..1eb6d75f 100644
--- a/components/notice/notice-client.tsx
+++ b/components/notice/notice-client.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState, useEffect, useTransition } from "react"
+import React, { useState, useEffect, useTransition } from "react"
import { useParams } from "next/navigation"
import { useTranslation } from "@/i18n/client"
import { Button } from "@/components/ui/button"
@@ -91,28 +91,12 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
const fetchNotices = async () => {
try {
setLoading(true)
- const search = searchQuery || undefined
startTransition(async () => {
- const result = await getNoticeLists({
- page: 1,
- perPage: 50,
- search: search,
- sort: [{ id: sortField, desc: sortDirection === "desc" }],
- flags: [],
- filters: [],
- joinOperator: "and",
- pagePath: "",
- title: "",
- content: "",
- authorId: null,
- isActive: null,
- from: "",
- to: "",
- })
+ const result = await getNoticeLists()
if (result?.data) {
- setNotices(result.data)
+ setNotices(result.data as NoticeWithAuthor[])
} else {
toast.error("공지사항 목록을 가져오는데 실패했습니다.")
}
@@ -125,37 +109,12 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
}
}
- // 검색 핸들러
+ // 검색 핸들러 (클라이언트 사이드에서 필터링하므로 별도 동작 불필요)
const handleSearch = () => {
- fetchNotices()
+ // 클라이언트 사이드 필터링이므로 별도 서버 요청 불필요
}
- // 정렬 함수
- const sortNotices = (notices: NoticeWithAuthor[]) => {
- return [...notices].sort((a, b) => {
- let aValue: string | Date
- let bValue: string | Date
- if (sortField === "title") {
- aValue = a.title
- bValue = b.title
- } else if (sortField === "pagePath") {
- aValue = a.pagePath
- bValue = b.pagePath
- } else {
- aValue = new Date(a.createdAt)
- bValue = new Date(b.createdAt)
- }
-
- if (aValue < bValue) {
- return sortDirection === "asc" ? -1 : 1
- }
- if (aValue > bValue) {
- return sortDirection === "asc" ? 1 : -1
- }
- return 0
- })
- }
// 정렬 핸들러
const handleSort = (field: SortField) => {
@@ -184,8 +143,49 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
}
}
- // 정렬된 공지사항 목록
- const sortedNotices = sortNotices(notices)
+ // 클라이언트 사이드 필터링 및 정렬
+ const filteredAndSortedNotices = React.useMemo(() => {
+ let filtered = notices
+
+ // 검색 필터
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase()
+ filtered = filtered.filter(notice =>
+ notice.title.toLowerCase().includes(query) ||
+ notice.pagePath.toLowerCase().includes(query) ||
+ notice.content.toLowerCase().includes(query)
+ )
+ }
+
+ // 정렬
+ filtered = filtered.sort((a, b) => {
+ let aValue: string | Date
+ let bValue: string | Date
+
+ switch (sortField) {
+ case "title":
+ aValue = a.title
+ bValue = b.title
+ break
+ case "pagePath":
+ aValue = a.pagePath
+ bValue = b.pagePath
+ break
+ case "createdAt":
+ aValue = new Date(a.createdAt)
+ bValue = new Date(b.createdAt)
+ break
+ default:
+ return 0
+ }
+
+ if (aValue < bValue) return sortDirection === "asc" ? -1 : 1
+ if (aValue > bValue) return sortDirection === "asc" ? 1 : -1
+ return 0
+ })
+
+ return filtered
+ }, [notices, searchQuery, sortField, sortDirection])
// 페이지 경로 옵션 로딩
const loadPagePathOptions = async () => {
@@ -227,13 +227,7 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
loadPagePathOptions()
}, [])
- useEffect(() => {
- if (searchQuery !== "") {
- fetchNotices()
- } else if (initialData.length > 0) {
- setNotices(initialData)
- }
- }, [searchQuery])
+ // 검색은 클라이언트 사이드에서 실시간으로 처리됨
return (
<div className="space-y-6">
@@ -243,16 +237,12 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
- placeholder="제목이나 페이지 경로로 검색..."
+ placeholder="제목, 페이지 경로, 내용으로 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
- onKeyPress={(e) => e.key === "Enter" && handleSearch()}
/>
</div>
- <Button onClick={handleSearch} variant="outline">
- 검색
- </Button>
<Button
variant="outline"
onClick={() => window.location.reload()}
@@ -328,14 +318,14 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr
로딩 중...
</TableCell>
</TableRow>
- ) : notices.length === 0 ? (
+ ) : filteredAndSortedNotices.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-gray-500">
- 공지사항이 없습니다.
+ {searchQuery.trim() ? "검색 결과가 없습니다." : "공지사항이 없습니다."}
</TableCell>
</TableRow>
) : (
- sortedNotices.map((notice) => (
+ filteredAndSortedNotices.map((notice) => (
<TableRow key={notice.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
diff --git a/components/notice/notice-create-dialog.tsx b/components/notice/notice-create-dialog.tsx
index 21cd46f6..591b2bc7 100644
--- a/components/notice/notice-create-dialog.tsx
+++ b/components/notice/notice-create-dialog.tsx
@@ -147,7 +147,7 @@ export function NoticeCreateDialog({
<SelectContent>
{pagePathOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
- {safeTranslate(option.label)}
+ {safeTranslate(option.label)} - {option.value}
</SelectItem>
))}
</SelectContent>
diff --git a/components/notice/notice-edit-sheet.tsx b/components/notice/notice-edit-sheet.tsx
index dc83d23a..b87714b3 100644
--- a/components/notice/notice-edit-sheet.tsx
+++ b/components/notice/notice-edit-sheet.tsx
@@ -170,7 +170,7 @@ export function UpdateNoticeSheet({
<SelectContent>
{pagePathOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
- {safeTranslate(option.label)}
+ {safeTranslate(option.label)} - {option.value}
</SelectItem>
))}
</SelectContent>
diff --git a/components/vendor-info/pq-simple-dialog.tsx b/components/vendor-info/pq-simple-dialog.tsx
index bb26685d..fbff2a1c 100644
--- a/components/vendor-info/pq-simple-dialog.tsx
+++ b/components/vendor-info/pq-simple-dialog.tsx
@@ -29,7 +29,7 @@ import { Download, FileText, ChevronDown, ChevronUp, Search } from "lucide-react
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
import { getPQProjectsByVendorId, ProjectPQ, getPQDataByVendorId, PQGroupData } from "@/lib/pq/service"
-import { downloadFile } from "@/lib/file-download"
+// downloadFile은 동적으로 import
interface PQSimpleDialogProps {
open: boolean
@@ -109,6 +109,9 @@ export function PQSimpleDialog({
const handleFileDownload = async (filePath: string, fileName: string) => {
try {
+ // 동적으로 downloadFile 함수 import
+ const { downloadFile } = await import('@/lib/file-download')
+
const result = await downloadFile(filePath, fileName)
if (result.success) {
toast.success(`${fileName} 파일이 다운로드되었습니다.`)
diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx
index 1b10760a..848e4977 100644
--- a/components/vendor-regular-registrations/document-status-dialog.tsx
+++ b/components/vendor-regular-registrations/document-status-dialog.tsx
@@ -15,7 +15,6 @@ import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrati
import {
documentStatusColumns,
} from "@/config/vendorRegularRegistrationsColumnsConfig";
-import { downloadFile } from "@/lib/file-download";
interface DocumentStatusDialogProps {
open: boolean;
@@ -73,51 +72,153 @@ export function DocumentStatusDialog({
if (!registration) return null;
// 파일 다운로드 핸들러
- // const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
- // try {
- // const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles];
- // if (!files || files.length === 0) {
- // toast.error("다운로드할 파일이 없습니다.");
- // return;
- // }
-
- // const file = files[fileIndex];
- // if (!file) {
- // toast.error("파일을 찾을 수 없습니다.");
- // return;
- // }
-
- // // filePath와 fileName 추출
- // const filePath = file.filePath || file.path;
- // const fileName = file.originalFileName || file.fileName || file.name;
-
- // if (!filePath || !fileName) {
- // toast.error("파일 정보가 올바르지 않습니다.");
- // return;
- // }
-
- // console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey });
-
- // // downloadFile 함수를 사용하여 파일 다운로드
- // const result = await downloadFile(filePath, fileName, {
- // showToast: true,
- // onError: (error) => {
- // console.error("파일 다운로드 오류:", error);
- // toast.error(`파일 다운로드 실패: ${error}`);
- // },
- // onSuccess: (fileName, fileSize) => {
- // console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize });
- // }
- // });
-
- // if (!result.success) {
- // console.error("파일 다운로드 실패:", result.error);
- // }
- // } catch (error) {
- // console.error("파일 다운로드 중 오류 발생:", error);
- // toast.error("파일 다운로드 중 오류가 발생했습니다.");
- // }
- // };
+ const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
+ try {
+ console.log(`🔍 파일 다운로드 시도:`, {
+ docKey,
+ fileIndex,
+ allDocumentFiles: registration.documentFiles,
+ registrationId: registration.id,
+ registrationKeys: Object.keys(registration),
+ fullRegistration: registration
+ });
+
+ // documentFiles가 없는 경우 처리
+ if (!registration.documentFiles) {
+ console.error(`❌ documentFiles가 없음:`, {
+ registration,
+ hasDocumentFiles: !!registration.documentFiles,
+ registrationKeys: Object.keys(registration)
+ });
+ toast.error("문서 파일 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요.");
+ return;
+ }
+
+ const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles];
+ console.log(`📂 ${docKey} 파일 목록:`, files);
+
+ if (!files || files.length === 0) {
+ console.warn(`❌ ${docKey}에 파일이 없음:`, { files, length: files?.length });
+ toast.error("다운로드할 파일이 없습니다.");
+ return;
+ }
+
+ const file = files[fileIndex];
+ console.log(`📄 선택된 파일 (index ${fileIndex}):`, file);
+
+ if (!file) {
+ console.warn(`❌ 파일 인덱스 ${fileIndex}에 파일이 없음`);
+ toast.error("파일을 찾을 수 없습니다.");
+ return;
+ }
+
+ // 파일 객체의 모든 속성 확인
+ console.log(`🔍 파일 객체 전체 속성:`, Object.keys(file));
+ console.log(`🔍 파일 상세 정보:`, {
+ filePath: file.filePath,
+ path: file.path,
+ originalFileName: file.originalFileName,
+ fileName: file.fileName,
+ name: file.name,
+ fullObject: file
+ });
+
+ // filePath와 fileName 추출
+ const filePath = file.filePath || file.path;
+ const fileName = file.originalFileName || file.fileName || file.name;
+
+ console.log(`📝 추출된 파일 정보:`, { filePath, fileName });
+
+ if (!filePath || !fileName) {
+ console.error(`❌ 파일 정보 누락:`, {
+ filePath,
+ fileName,
+ fileObject: file,
+ availableKeys: Object.keys(file)
+ });
+ toast.error("파일 정보가 올바르지 않습니다.");
+ return;
+ }
+
+ console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey });
+
+ // downloadFile 함수를 동적으로 import하여 파일 다운로드
+ const { downloadFile } = await import('@/lib/file-download');
+ const result = await downloadFile(filePath, fileName, {
+ showToast: true,
+ onError: (error: any) => {
+ console.error("파일 다운로드 오류:", error);
+ toast.error(`파일 다운로드 실패: ${error}`);
+ },
+ onSuccess: (fileName: string, fileSize?: number) => {
+ console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize });
+ }
+ });
+
+ if (!result.success) {
+ console.error("파일 다운로드 실패:", result.error);
+ }
+ } catch (error) {
+ console.error("파일 다운로드 중 오류 발생:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+ };
+
+ // 기본계약 파일 다운로드 핸들러
+ const handleContractDownload = async (contractIndex: number) => {
+ try {
+ if (!registration.basicContracts || registration.basicContracts.length === 0) {
+ toast.error("다운로드할 계약이 없습니다.");
+ return;
+ }
+
+ const contract = registration.basicContracts[contractIndex];
+ if (!contract) {
+ toast.error("계약을 찾을 수 없습니다.");
+ return;
+ }
+
+ if (contract.status !== "COMPLETED") {
+ toast.error("완료된 계약서만 다운로드할 수 있습니다.");
+ return;
+ }
+
+ // 서명된 계약서 파일 정보 확인
+ const filePath = contract.filePath;
+ const fileName = contract.fileName || `${contract.templateName || '기본계약'}_${registration.companyName}.docx`;
+
+ if (!filePath) {
+ toast.error("계약서 파일을 찾을 수 없습니다.");
+ return;
+ }
+
+ console.log(`📥 기본계약 다운로드 시작:`, {
+ filePath,
+ fileName,
+ templateName: contract.templateName
+ });
+
+ // downloadFile 함수를 사용하여 서명된 계약서 다운로드
+ const { downloadFile } = await import('@/lib/file-download');
+ const result = await downloadFile(filePath, fileName, {
+ showToast: true,
+ onError: (error: any) => {
+ console.error("기본계약 다운로드 오류:", error);
+ toast.error(`기본계약 다운로드 실패: ${error}`);
+ },
+ onSuccess: (fileName: string, fileSize?: number) => {
+ console.log(`✅ 기본계약 다운로드 성공:`, { fileName, fileSize });
+ }
+ });
+
+ if (!result.success) {
+ console.error("기본계약 다운로드 실패:", result.error);
+ }
+ } catch (error) {
+ console.error("기본계약 다운로드 중 오류 발생:", error);
+ toast.error("기본계약 다운로드 중 오류가 발생했습니다.");
+ }
+ };
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -205,7 +306,7 @@ export function DocumentStatusDialog({
{isSubmitted ? "2024.01.01" : "-"}
</div>
<div>
- {/* {isSubmitted && (
+ {isSubmitted && (
<Button
size="sm"
variant="outline"
@@ -214,7 +315,7 @@ export function DocumentStatusDialog({
<Download className="w-4 h-4 mr-1" />
다운로드
</Button>
- )} */}
+ )}
</div>
</div>
);
@@ -262,7 +363,11 @@ export function DocumentStatusDialog({
</div>
<div>
{isCompleted && (
- <Button size="sm" variant="outline">
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleContractDownload(index)}
+ >
<Download className="w-4 h-4 mr-1" />
다운로드
</Button>
diff --git a/config/vendorRegularRegistrationsColumnsConfig.ts b/config/vendorRegularRegistrationsColumnsConfig.ts
index f9567883..44f02f96 100644
--- a/config/vendorRegularRegistrationsColumnsConfig.ts
+++ b/config/vendorRegularRegistrationsColumnsConfig.ts
@@ -39,6 +39,8 @@ export interface VendorRegularRegistration {
templateName: string | null;
status: string;
createdAt: Date | null;
+ filePath: string | null;
+ fileName: string | null;
}>;
registrationRequestDate: string | null;
assignedDepartment: string | null;
diff --git a/lib/information/service.ts b/lib/information/service.ts
index 2826c0e9..2d3ad079 100644
--- a/lib/information/service.ts
+++ b/lib/information/service.ts
@@ -1,8 +1,6 @@
"use server"
-import { revalidateTag } from "next/cache"
import { getErrorMessage } from "@/lib/handle-error"
-import { unstable_cache } from "@/lib/unstable-cache"
import { desc, or, eq } from "drizzle-orm"
import db from "@/db/db"
import { pageInformation, menuAssignments } from "@/db/schema"
@@ -45,22 +43,29 @@ export async function getInformationLists() {
// 페이지별 인포메이션 조회 (첨부파일 포함)
export async function getPageInformation(pagePath: string) {
try {
- return await getInformationByPagePathWithAttachments(pagePath)
+ console.log('🔍 Information Service - 조회 시작:', { pagePath })
+ const result = await getInformationByPagePathWithAttachments(pagePath)
+ console.log('📊 Information Service - 조회 결과:', {
+ pagePath,
+ found: !!result,
+ resultData: result ? {
+ id: result.id,
+ pagePath: result.pagePath,
+ pageName: result.pageName,
+ attachmentsCount: result.attachments?.length || 0
+ } : null
+ })
+ return result
} catch (error) {
console.error(`Failed to get information for page ${pagePath}:`, error)
return null
}
}
-// 캐시된 페이지별 인포메이션 조회
-export const getCachedPageInformation = unstable_cache(
- async (pagePath: string) => getPageInformation(pagePath),
- ["page-information"],
- {
- tags: ["page-information"],
- revalidate: 3600, // 1시간 캐시
- }
-)
+// 페이지별 인포메이션 조회 (직접 호출용)
+export async function getPageInformationDirect(pagePath: string) {
+ return await getPageInformation(pagePath)
+}
// 인포메이션 수정 (내용과 첨부파일만)
export async function updateInformationData(input: UpdateInformationSchema) {
@@ -83,9 +88,7 @@ export async function updateInformationData(input: UpdateInformationSchema) {
}
}
- revalidateTag("page-information")
- revalidateTag("information-lists")
- revalidateTag("information-edit-permission") // 편집 권한 캐시 무효화
+ // 캐시 무효화 제거됨
return {
success: true,
@@ -113,13 +116,18 @@ export async function getInformationDetail(id: number) {
// 인포메이션 편집 권한 확인
export async function checkInformationEditPermission(pagePath: string, userId: string): Promise<boolean> {
try {
+ // pagePath 정규화 (앞의 / 제거)
+ const normalizedPagePath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath
+
// pagePath를 menuPath로 변환 (pagePath가 menuPath의 마지막 부분이라고 가정)
// 예: pagePath "vendor-list" -> menuPath "/evcp/vendor-list" 또는 "/partners/vendor-list"
const menuPathQueries = [
- `/evcp/${pagePath}`,
- `/partners/${pagePath}`,
- `/${pagePath}`, // 루트 경로
- pagePath // 정확한 매칭
+ `/evcp/${normalizedPagePath}`,
+ `/partners/${normalizedPagePath}`,
+ `/${normalizedPagePath}`, // 루트 경로
+ normalizedPagePath, // 정확한 매칭
+ `/${pagePath}`, // 원본 경로도 체크
+ pagePath // 원본 경로 정확한 매칭
]
// menu_assignments에서 해당 pagePath와 매칭되는 메뉴 찾기
@@ -149,15 +157,10 @@ export async function checkInformationEditPermission(pagePath: string, userId: s
}
}
-// 캐시된 권한 확인
-export const getCachedEditPermission = unstable_cache(
- async (pagePath: string, userId: string) => checkInformationEditPermission(pagePath, userId),
- ["information-edit-permission"],
- {
- tags: ["information-edit-permission"],
- revalidate: 300, // 5분 캐시
- }
-)
+// 권한 확인 (직접 호출용)
+export async function getEditPermissionDirect(pagePath: string, userId: string) {
+ return await checkInformationEditPermission(pagePath, userId)
+}
// menu_assignments 기반으로 page_information 동기화
export async function syncInformationFromMenuAssignments() {
@@ -170,9 +173,14 @@ export async function syncInformationFromMenuAssignments() {
// upsert를 사용하여 각 메뉴 항목 처리
for (const menu of menuItems) {
try {
+ // 맨 앞의 / 제거하여 pagePath 정규화
+ const normalizedPagePath = menu.menuPath.startsWith('/')
+ ? menu.menuPath.slice(1)
+ : menu.menuPath;
+
await db.insert(pageInformation)
.values({
- pagePath: menu.menuPath,
+ pagePath: normalizedPagePath,
pageName: menu.menuTitle,
informationContent: "",
isActive: true // 기본값으로 활성화
@@ -191,7 +199,7 @@ export async function syncInformationFromMenuAssignments() {
}
}
- revalidateTag("information");
+ // 캐시 무효화 제거됨
return {
success: true,
@@ -249,8 +257,7 @@ export async function uploadInformationAttachment(formData: FormData) {
}
}
- revalidateTag("page-information")
- revalidateTag("information-lists")
+ // 캐시 무효화 제거됨
return {
success: true,
@@ -288,8 +295,7 @@ export async function deleteInformationAttachmentAction(attachmentId: number) {
}
}
- revalidateTag("page-information")
- revalidateTag("information-lists")
+ // 캐시 무효화 제거됨
return {
success: true,
diff --git a/lib/information/table/update-information-dialog.tsx b/lib/information/table/update-information-dialog.tsx
index a02b6eb1..370eb763 100644
--- a/lib/information/table/update-information-dialog.tsx
+++ b/lib/information/table/update-information-dialog.tsx
@@ -44,7 +44,7 @@ import {
downloadInformationAttachment
} from "@/lib/information/service"
import type { PageInformation, InformationAttachment } from "@/db/schema/information"
-import { downloadFile } from "@/lib/file-download"
+// downloadFile은 동적으로 import
import prettyBytes from "pretty-bytes"
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
@@ -126,6 +126,8 @@ export function UpdateInformationDialog({
try {
const result = await downloadInformationAttachment(attachment.id)
if (result.success && result.data) {
+ // 동적으로 downloadFile 함수 import
+ const { downloadFile } = await import('@/lib/file-download')
await downloadFile(result.data.filePath, result.data.fileName)
toast.success("파일 다운로드가 시작되었습니다.")
} else {
diff --git a/lib/notice/repository.ts b/lib/notice/repository.ts
index 84e64f00..fb941ac9 100644
--- a/lib/notice/repository.ts
+++ b/lib/notice/repository.ts
@@ -1,160 +1,7 @@
-import { asc, desc, eq, ilike, and, count, sql } from "drizzle-orm"
+import { desc, eq, and, sql } from "drizzle-orm"
import db from "@/db/db"
import { notice, users, type Notice, type NewNotice } from "@/db/schema"
-// 최신 패턴: 트랜잭션을 지원하는 공지사항 조회
-export async function selectNoticeLists(
- tx: typeof db,
- params: {
- where?: ReturnType<typeof and>
- orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[]
- offset?: number
- limit?: number
- }
-) {
- const { where, orderBy, offset = 0, limit = 10 } = params
-
- return tx
- .select({
- id: notice.id,
- pagePath: notice.pagePath,
- title: notice.title,
- content: notice.content,
- authorId: notice.authorId,
- isActive: notice.isActive,
- createdAt: notice.createdAt,
- updatedAt: notice.updatedAt,
- authorName: users.name,
- authorEmail: users.email,
- })
- .from(notice)
- .leftJoin(users, eq(notice.authorId, users.id))
- .where(where)
- .orderBy(...(orderBy ?? [desc(notice.createdAt)]))
- .offset(offset)
- .limit(limit)
-}
-
-// 최신 패턴: 트랜잭션을 지원하는 카운트 조회
-export async function countNoticeLists(
- tx: typeof db,
- where?: ReturnType<typeof and>
-) {
- const res = await tx
- .select({ count: count() })
- .from(notice)
- .where(where)
-
- return res[0]?.count ?? 0
-}
-
-// 기존 패턴 (하위 호환성을 위해 유지)
-export async function selectNotice(input: { page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) {
- const { page, per_page = 50, sort, pagePath, title, authorId, isActive, from, to } = input
-
- const conditions = []
-
- if (pagePath) {
- conditions.push(ilike(notice.pagePath, `%${pagePath}%`))
- }
-
- if (title) {
- conditions.push(ilike(notice.title, `%${title}%`))
- }
-
- if (authorId) {
- conditions.push(eq(notice.authorId, authorId))
- }
-
- if (isActive !== null && isActive !== undefined) {
- conditions.push(eq(notice.isActive, isActive))
- }
-
- if (from) {
- conditions.push(sql`${notice.createdAt} >= ${from}`)
- }
-
- if (to) {
- conditions.push(sql`${notice.createdAt} <= ${to}`)
- }
-
- const offset = (page - 1) * per_page
-
- // 정렬 설정
- let orderBy = desc(notice.createdAt);
-
- if (sort && Array.isArray(sort) && sort.length > 0) {
- const sortItem = sort[0];
- if (sortItem.id === "createdAt") {
- orderBy = sortItem.desc ? desc(notice.createdAt) : asc(notice.createdAt);
- }
- }
-
- const whereClause = conditions.length > 0 ? and(...conditions) : undefined
-
- const data = await db
- .select({
- id: notice.id,
- pagePath: notice.pagePath,
- title: notice.title,
- content: notice.content,
- authorId: notice.authorId,
- isActive: notice.isActive,
- createdAt: notice.createdAt,
- updatedAt: notice.updatedAt,
- authorName: users.name,
- authorEmail: users.email,
- })
- .from(notice)
- .leftJoin(users, eq(notice.authorId, users.id))
- .where(whereClause)
- .orderBy(orderBy)
- .limit(per_page)
- .offset(offset)
-
- return data
-}
-
-// 기존 패턴: 공지사항 총 개수 조회
-export async function countNotice(input: { pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }) {
- const { pagePath, title, authorId, isActive, from, to } = input
-
- const conditions = []
-
- if (pagePath) {
- conditions.push(ilike(notice.pagePath, `%${pagePath}%`))
- }
-
- if (title) {
- conditions.push(ilike(notice.title, `%${title}%`))
- }
-
- if (authorId) {
- conditions.push(eq(notice.authorId, authorId))
- }
-
- if (isActive !== null && isActive !== undefined) {
- conditions.push(eq(notice.isActive, isActive))
- }
-
- if (from) {
- conditions.push(sql`${notice.createdAt} >= ${from}`)
- }
-
- if (to) {
- conditions.push(sql`${notice.createdAt} <= ${to}`)
- }
-
- const whereClause = conditions.length > 0 ? and(...conditions) : undefined
-
- const result = await db
- .select({ count: count() })
- .from(notice)
- .where(whereClause)
-
- return result[0]?.count ?? 0
-}
-
// 페이지 경로별 공지사항 조회 (활성화된 것만, 작성자 정보 포함)
export async function getNoticesByPagePath(pagePath: string): Promise<Array<Notice & { authorName: string | null; authorEmail: string | null }>> {
const result = await db
diff --git a/lib/notice/service.ts b/lib/notice/service.ts
index c261cd2e..9c05b98f 100644
--- a/lib/notice/service.ts
+++ b/lib/notice/service.ts
@@ -1,203 +1,81 @@
"use server"
-import { revalidateTag, unstable_noStore } from "next/cache"
import { getErrorMessage } from "@/lib/handle-error"
-import { unstable_cache } from "@/lib/unstable-cache"
-import { filterColumns } from "@/lib/filter-columns"
-import { asc, desc, ilike, and, or, eq } from "drizzle-orm"
+import { desc, eq } from "drizzle-orm"
import db from "@/db/db"
-import { notice, pageInformation, menuAssignments } from "@/db/schema"
+import { notice, pageInformation, menuAssignments, users } from "@/db/schema"
import type {
CreateNoticeSchema,
- UpdateNoticeSchema,
- GetNoticeSchema
+ UpdateNoticeSchema
} from "./validations"
import {
- selectNotice,
- countNotice,
getNoticesByPagePath,
insertNotice,
updateNotice,
deleteNoticeById,
deleteNoticeByIds,
- getNoticeById,
- selectNoticeLists,
- countNoticeLists
+ getNoticeById
} from "./repository"
import type { Notice } from "@/db/schema/notice"
-export async function getNoticeLists(input: GetNoticeSchema) {
- return unstable_cache(
- async () => {
- try {
- // 고급 검색 로직
- const { page, perPage, search, filters, joinOperator, pagePath, title, content, authorId, isActive } = input
-
- // 기본 검색 조건들
- const conditions = []
-
- // 검색어가 있으면 여러 필드에서 검색
- if (search && search.trim()) {
- const searchConditions = [
- ilike(notice.pagePath, `%${search}%`),
- ilike(notice.title, `%${search}%`),
- ilike(notice.content, `%${search}%`)
- ]
- conditions.push(or(...searchConditions))
- }
-
- // 개별 필드 조건들
- if (pagePath && pagePath.trim()) {
- conditions.push(ilike(notice.pagePath, `%${pagePath}%`))
- }
-
- if (title && title.trim()) {
- conditions.push(ilike(notice.title, `%${title}%`))
- }
-
- if (content && content.trim()) {
- conditions.push(ilike(notice.content, `%${content}%`))
- }
-
- if (authorId !== null && authorId !== undefined) {
- conditions.push(eq(notice.authorId, authorId))
- }
-
- if (isActive !== null && isActive !== undefined) {
- conditions.push(eq(notice.isActive, isActive))
- }
- // 고급 필터 처리
- if (filters && filters.length > 0) {
- const advancedConditions = filters.map(() =>
- filterColumns({
- table: notice,
- filters: filters,
- joinOperator: joinOperator,
- })
- )
-
- if (advancedConditions.length > 0) {
- if (joinOperator === "or") {
- conditions.push(or(...advancedConditions))
- } else {
- conditions.push(and(...advancedConditions))
- }
- }
- }
-
- // 전체 WHERE 조건 조합
- const finalWhere = conditions.length > 0
- ? (joinOperator === "or" ? or(...conditions) : and(...conditions))
- : undefined
-
- // 페이지네이션
- const offset = (page - 1) * perPage
-
- // 정렬 처리
- const orderBy = input.sort.length > 0
- ? input.sort.map((item) => {
- if (item.id === "createdAt") {
- return item.desc ? desc(notice.createdAt) : asc(notice.createdAt)
- } else if (item.id === "updatedAt") {
- return item.desc ? desc(notice.updatedAt) : asc(notice.updatedAt)
- } else if (item.id === "pagePath") {
- return item.desc ? desc(notice.pagePath) : asc(notice.pagePath)
- } else if (item.id === "title") {
- return item.desc ? desc(notice.title) : asc(notice.title)
- } else if (item.id === "authorId") {
- return item.desc ? desc(notice.authorId) : asc(notice.authorId)
- } else if (item.id === "isActive") {
- return item.desc ? desc(notice.isActive) : asc(notice.isActive)
- } else {
- return desc(notice.createdAt) // 기본값
- }
- })
- : [desc(notice.createdAt)]
-
- // 트랜잭션 내부에서 Repository 호출
- const { data, total } = await db.transaction(async (tx) => {
- const data = await selectNoticeLists(tx, {
- where: finalWhere,
- orderBy,
- offset,
- limit: input.perPage,
- })
-
- const total = await countNoticeLists(tx, finalWhere)
- return { data, total }
- })
-
- const pageCount = Math.ceil(total / input.perPage)
-
- return { data, pageCount, total }
- } catch (err) {
- console.error("Failed to get notice lists:", err)
- // 에러 발생 시 기본값 반환
- return { data: [], pageCount: 0, total: 0 }
- }
- },
- [JSON.stringify(input)],
- {
- revalidate: 3600,
- tags: ["notice-lists"],
- }
- )()
-}
-
-// 기존 패턴 (하위 호환성을 위해 유지)
-export async function getNoticeList(input: Partial<{ page: number; per_page: number; sort?: string; pagePath?: string; title?: string; authorId?: number; isActive?: boolean; from?: string; to?: string }> & { page: number; per_page: number }) {
- unstable_noStore()
-
+// 간단한 공지사항 목록 조회 (페이지네이션 없이 전체 조회)
+export async function getNoticeLists(): Promise<{ data: Array<Notice & { authorName: string | null; authorEmail: string | null }> }> {
try {
- const [data, total] = await Promise.all([
- selectNotice(input),
- countNotice(input)
- ])
-
- const pageCount = Math.ceil(total / input.per_page)
-
- return {
- data,
- pageCount,
- total
- }
- } catch (error) {
- console.error("Failed to get notice list:", error)
- throw new Error(getErrorMessage(error))
+ // 전체 데이터 조회 (작성자 정보 포함, 클라이언트에서 검색 처리)
+ const data = await db
+ .select({
+ id: notice.id,
+ pagePath: notice.pagePath,
+ title: notice.title,
+ content: notice.content,
+ authorId: notice.authorId,
+ isActive: notice.isActive,
+ createdAt: notice.createdAt,
+ updatedAt: notice.updatedAt,
+ authorName: users.name,
+ authorEmail: users.email,
+ })
+ .from(notice)
+ .leftJoin(users, eq(notice.authorId, users.id))
+ .orderBy(desc(notice.createdAt))
+
+ return { data }
+ } catch (err) {
+ console.error("Failed to get notice lists:", err)
+ return { data: [] }
}
}
// 페이지별 공지사항 조회 (일반 사용자용)
export async function getPageNotices(pagePath: string): Promise<Array<Notice & { authorName: string | null; authorEmail: string | null }>> {
try {
- return await getNoticesByPagePath(pagePath)
+ console.log('🔍 Notice Service - 조회 시작:', { pagePath })
+ const result = await getNoticesByPagePath(pagePath)
+ console.log('📊 Notice Service - 조회 결과:', {
+ pagePath,
+ noticesCount: result.length,
+ notices: result.map(n => ({ id: n.id, title: n.title, pagePath: n.pagePath }))
+ })
+ return result
} catch (error) {
console.error(`Failed to get notices for page ${pagePath}:`, error)
return []
}
}
-// 캐시된 페이지별 공지사항 조회
-export const getCachedPageNotices = unstable_cache(
- async (pagePath: string) => getPageNotices(pagePath),
- ["page-notices"],
- {
- tags: ["page-notices"],
- revalidate: 3600, // 1시간 캐시
- }
-)
+// 페이지별 공지사항 조회 (직접 호출용)
+export async function getPageNoticesDirect(pagePath: string) {
+ return await getPageNotices(pagePath)
+}
// 공지사항 생성
export async function createNotice(input: CreateNoticeSchema) {
try {
const result = await insertNotice(input)
- revalidateTag("page-notices")
- revalidateTag("notice-lists")
-
return {
success: true,
data: result,
@@ -225,9 +103,6 @@ export async function updateNoticeData(input: UpdateNoticeSchema) {
}
}
- revalidateTag("page-notices")
- revalidateTag("notice-lists")
-
return {
success: true,
message: "공지사항이 성공적으로 수정되었습니다."
@@ -253,9 +128,6 @@ export async function deleteNotice(id: number) {
}
}
- revalidateTag("page-notices")
- revalidateTag("notice-lists")
-
return {
success: true,
message: "공지사항이 성공적으로 삭제되었습니다."
@@ -274,9 +146,6 @@ export async function deleteMultipleNotices(ids: number[]) {
try {
const deletedCount = await deleteNoticeByIds(ids)
- revalidateTag("page-notices")
- revalidateTag("notice-lists")
-
return {
success: true,
deletedCount,
@@ -311,7 +180,7 @@ export async function getPagePathList(): Promise<Array<{ pagePath: string; pageN
})
.from(pageInformation)
.where(eq(pageInformation.isActive, true))
- .orderBy(asc(pageInformation.pagePath))
+ .orderBy(desc(pageInformation.pagePath))
return result.map(item => ({
pagePath: item.pagePath,
@@ -349,8 +218,6 @@ export async function syncNoticeFromMenuAssignments() {
processedCount++;
}
- revalidateTag("notice");
-
return {
success: true,
message: `공지사항 경로 동기화 확인 완료: ${processedCount}개 확인, ${missingPaths.length}개 누락`,
diff --git a/lib/notice/validations.ts b/lib/notice/validations.ts
index 05e84af9..146f8e09 100644
--- a/lib/notice/validations.ts
+++ b/lib/notice/validations.ts
@@ -1,14 +1,4 @@
import { z } from "zod"
-import {
- createSearchParamsCache,
- parseAsArrayOf,
- parseAsInteger,
- parseAsString,
- parseAsStringEnum,
- parseAsBoolean,
-} from "nuqs/server"
-import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
-import { Notice } from "@/db/schema/notice"
// 공지사항 생성 스키마
export const createNoticeSchema = z.object({
@@ -28,53 +18,12 @@ export const updateNoticeSchema = z.object({
isActive: z.boolean().default(true),
})
-// 현대적인 검색 파라미터 캐시
-export const searchParamsNoticeCache = createSearchParamsCache({
- flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
- page: parseAsInteger.withDefault(1),
- perPage: parseAsInteger.withDefault(10),
- sort: getSortingStateParser<Notice>().withDefault([
- { id: "createdAt", desc: true },
- ]),
-
- // 기본 검색 필드들
- pagePath: parseAsString.withDefault(""),
- title: parseAsString.withDefault(""),
- content: parseAsString.withDefault(""),
- authorId: parseAsInteger,
- isActive: parseAsBoolean,
-
- // 고급 필터
- filters: getFiltersStateParser().withDefault([]),
- joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
- search: parseAsString.withDefault(""),
-
- // 날짜 범위
- from: parseAsString.withDefault(""),
- to: parseAsString.withDefault(""),
-})
-
-// 타입 추출
-export type CreateNoticeSchema = z.infer<typeof createNoticeSchema>
-export type UpdateNoticeSchema = z.infer<typeof updateNoticeSchema>
-export type GetNoticeSchema = Awaited<ReturnType<typeof searchParamsNoticeCache.parse>>
-
-// 기존 스키마 (하위 호환성을 위해 유지)
-export const getNoticeSchema = z.object({
- page: z.coerce.number().default(1),
- per_page: z.coerce.number().default(10),
- sort: z.string().optional(),
- pagePath: z.string().optional(),
- title: z.string().optional(),
- authorId: z.coerce.number().optional(),
- isActive: z.coerce.boolean().optional(),
- from: z.string().optional(),
- to: z.string().optional(),
-})
-
// 페이지 경로별 공지사항 조회 스키마
export const getPageNoticeSchema = z.object({
pagePath: z.string().min(1, "페이지 경로를 입력해주세요"),
})
+// 타입 추출
+export type CreateNoticeSchema = z.infer<typeof createNoticeSchema>
+export type UpdateNoticeSchema = z.infer<typeof updateNoticeSchema>
export type GetPageNoticeSchema = z.infer<typeof getPageNoticeSchema> \ No newline at end of file
diff --git a/lib/vendor-regular-registrations/repository.ts b/lib/vendor-regular-registrations/repository.ts
index 38bf4aaf..aec3d275 100644
--- a/lib/vendor-regular-registrations/repository.ts
+++ b/lib/vendor-regular-registrations/repository.ts
@@ -75,6 +75,8 @@ export async function getVendorRegularRegistrations(
status: basicContract.status,
templateName: basicContractTemplates.templateName,
createdAt: basicContract.createdAt,
+ filePath: basicContract.filePath,
+ fileName: basicContract.fileName,
})
.from(basicContract)
.leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id))
@@ -153,6 +155,35 @@ export async function getVendorRegularRegistrations(
auditResult: investigationFiles,
};
+ // 디버깅용 로그 추가
+ console.log(`🔍 벤더 ID ${registration.vendorId} documentFiles 구조:`, {
+ businessRegistration: documentFiles.businessRegistration.map(f => ({
+ fileName: f.fileName,
+ filePath: f.filePath,
+ attachmentType: f.attachmentType,
+ allKeys: Object.keys(f)
+ })),
+ creditEvaluation: documentFiles.creditEvaluation.map(f => ({
+ fileName: f.fileName,
+ filePath: f.filePath,
+ attachmentType: f.attachmentType,
+ allKeys: Object.keys(f)
+ })),
+ bankCopy: documentFiles.bankCopy.map(f => ({
+ fileName: f.fileName,
+ filePath: f.filePath,
+ attachmentType: f.attachmentType,
+ allKeys: Object.keys(f)
+ })),
+ auditResult: documentFiles.auditResult.map(f => ({
+ fileName: f.fileName,
+ attachmentType: f.attachmentType,
+ allKeys: Object.keys(f)
+ })),
+ totalVendorFiles: vendorFiles.length,
+ totalInvestigationFiles: investigationFiles.length
+ });
+
// 문서 제출 현황 로그
console.log(`📊 벤더 ID ${registration.vendorId} 문서 제출 현황:`, {
documentSubmissionsStatus,
@@ -230,11 +261,13 @@ export async function getVendorRegularRegistrations(
gtcSkipped: registration.gtcSkipped || false,
additionalInfo: additionalInfoCompleted,
// 기본계약 정보
- basicContracts: vendorContracts.map(contract => ({
+ basicContracts: vendorContracts.map((contract: any) => ({
templateId: contract.templateId,
templateName: contract.templateName,
status: contract.status,
createdAt: contract.createdAt,
+ filePath: contract.filePath,
+ fileName: contract.fileName,
})),
registrationRequestDate: registration.registrationRequestDate || null,
assignedDepartment: registration.assignedDepartment,
diff --git a/lib/vendor-regular-registrations/service.ts b/lib/vendor-regular-registrations/service.ts
index d64c7b8b..7ec433b4 100644
--- a/lib/vendor-regular-registrations/service.ts
+++ b/lib/vendor-regular-registrations/service.ts
@@ -714,6 +714,8 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
const investigationFiles = await db
.select({
attachmentId: vendorInvestigationAttachments.id,
+ fileName: vendorInvestigationAttachments.fileName,
+ filePath: vendorInvestigationAttachments.filePath,
createdAt: vendorInvestigationAttachments.createdAt,
})
.from(vendorInvestigationAttachments)
@@ -735,6 +737,8 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
templateName: basicContractTemplates.templateName,
status: basicContract.status,
createdAt: basicContract.createdAt,
+ filePath: basicContract.filePath,
+ fileName: basicContract.fileName,
})
.from(basicContract)
.leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id))
@@ -796,6 +800,14 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
safetyQualification: investigationFiles.length > 0,
}
+ // 문서별 파일 정보 (다운로드용)
+ const documentFiles = {
+ businessRegistration: vendorFiles.filter(f => f.attachmentType === "BUSINESS_REGISTRATION"),
+ creditEvaluation: vendorFiles.filter(f => f.attachmentType === "CREDIT_REPORT"),
+ bankCopy: vendorFiles.filter(f => f.attachmentType === "BANK_ACCOUNT_COPY"),
+ auditResult: investigationFiles,
+ }
+
// 미완성 항목 계산
const missingDocuments = Object.entries(documentStatus)
.filter(([, value]) => !value)
@@ -831,6 +843,7 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
vendor: vendor[0],
registration: registration[0] || null,
documentStatus,
+ documentFiles, // 문서별 파일 정보 추가
missingDocuments,
businessContacts,
missingContactTypes,