summaryrefslogtreecommitdiff
path: root/components/pq-input
diff options
context:
space:
mode:
Diffstat (limited to 'components/pq-input')
-rw-r--r--components/pq-input/pq-input-tabs.tsx90
-rw-r--r--components/pq-input/pq-review-wrapper.tsx78
2 files changed, 159 insertions, 9 deletions
diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx
index df911d5e..6c9a1254 100644
--- a/components/pq-input/pq-input-tabs.tsx
+++ b/components/pq-input/pq-input-tabs.tsx
@@ -152,6 +152,7 @@ export function PQInputTabs({
projectData,
isReadOnly = false,
currentPQ, // 추가: 현재 PQ Submission 정보
+ vendorCountry,
}: {
data: PQGroupData[]
vendorId: number
@@ -163,6 +164,7 @@ export function PQInputTabs({
status: string;
type: string;
} | null
+ vendorCountry?: string | null
}) {
const [isSaving, setIsSaving] = React.useState(false)
@@ -208,6 +210,33 @@ export function PQInputTabs({
})
}
+ // 벤더 내자/외자 판별 (국가 코드 기반)
+ const isDomesticVendor = React.useMemo(() => {
+ if (!vendorCountry) return null; // null 이면 필터 미적용
+ return vendorCountry === "KR" || vendorCountry === "한국";
+ }, [vendorCountry]);
+
+ // 벤더 유형에 따라 PQ 항목 필터링
+ const filteredData: PQGroupData[] = React.useMemo(() => {
+ // 벤더 타입 정보가 없으면 전체 노출
+ if (isDomesticVendor === null) return data;
+
+ const filterItemByType = (item: any) => {
+ const itemType = item.type || "내외자";
+ if (itemType === "내외자") return true;
+ if (itemType === "내자") return isDomesticVendor === true;
+ if (itemType === "외자") return isDomesticVendor === false;
+ return true;
+ };
+
+ return data
+ .map((group) => ({
+ ...group,
+ items: group.items.filter(filterItemByType),
+ }))
+ .filter((group) => group.items.length > 0);
+ }, [data, isDomesticVendor]);
+
// 필터링 함수
const shouldShowItem = (isSaved: boolean) => {
if (filterOptions.showAll) return true;
@@ -223,7 +252,7 @@ export function PQInputTabs({
function createInitialFormValues(): PQFormValues {
const answers: PQFormValues["answers"] = []
- data.forEach((group) => {
+ filteredData.forEach((group) => {
// 그룹 내 아이템들을 코드 순서로 정렬
const sortedItems = sortByCode(group.items)
@@ -383,7 +412,7 @@ export function PQInputTabs({
try {
const answerData = form.getValues(`answers.${answerIndex}`)
const criteriaId = answerData.criteriaId
- const item = data.flatMap(group => group.items).find(item => item.criteriaId === criteriaId)
+ const item = filteredData.flatMap(group => group.items).find(item => item.criteriaId === criteriaId)
const inputFormat = item?.inputFormat || "TEXT"
// Validation
// 모든 항목은 필수로 처리 (isRequired 제거됨)
@@ -723,7 +752,14 @@ export function PQInputTabs({
{/* 프로젝트 정보 섹션 */}
{renderProjectInfo()}
- <Tabs defaultValue={data[0]?.groupName || ""} className="w-full">
+ {filteredData.length === 0 ? (
+ <div className="rounded-md border border-dashed p-6 text-sm text-muted-foreground">
+ 표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용)
+ </div>
+ ) : (
+ <>
+
+ <Tabs defaultValue={filteredData[0]?.groupName || ""} className="w-full">
{/* Top Controls - Sticky Header */}
<div className="sticky top-0 z-10 bg-background border-b border-border mb-4 pb-4">
{/* Item Count Display */}
@@ -810,7 +846,7 @@ export function PQInputTabs({
<div className="flex justify-between items-center">
<TabsList className="grid grid-cols-4">
- {data.map((group) => {
+ {filteredData.map((group) => {
const colorClasses = getTabColorClasses(group.groupName)
return (
<TabsTrigger
@@ -880,7 +916,7 @@ export function PQInputTabs({
</div>
{/* Render each group */}
- {data.map((group) => (
+ {filteredData.map((group) => (
<TabsContent key={group.groupName} value={group.groupName}>
{/* 2-column grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-4">
@@ -958,6 +994,46 @@ export function PQInputTabs({
</CardHeader>
<CardContent className="pt-3 space-y-3 h-full flex flex-col">
+ {/* 기준 첨부 파일 */}
+ {item.criteriaAttachments && item.criteriaAttachments.length > 0 && (
+ <div className="space-y-2">
+ <FormLabel>기준 첨부파일</FormLabel>
+ <FileList>
+ {item.criteriaAttachments.map((file, idx) => (
+ <FileListItem key={idx}>
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{file.fileName}</FileListName>
+ {file.fileSize && (
+ <FileListDescription>{prettyBytes(file.fileSize)}</FileListDescription>
+ )}
+ </FileListInfo>
+ <FileListAction
+ onClick={async () => {
+ try {
+ const { downloadFile } = await import('@/lib/file-download')
+ await downloadFile(file.filePath, file.fileName, { showToast: true })
+ } catch (error) {
+ console.error('다운로드 오류:', error)
+ toast({
+ title: "다운로드 실패",
+ description: "파일 다운로드 중 오류가 발생했습니다.",
+ variant: "destructive"
+ })
+ }
+ }}
+ >
+ <Download className="h-4 w-4" />
+ <span className="sr-only">Download</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ </div>
+ )}
+
{/* 프로젝트별 추가 필드 (contractInfo, additionalRequirement) */}
{projectId && contractInfo && (
<div className="space-y-1">
@@ -1379,6 +1455,8 @@ export function PQInputTabs({
</TabsContent>
))}
</Tabs>
+ </>
+ )}
</form>
{/* Confirmation Dialog */}
@@ -1395,7 +1473,7 @@ export function PQInputTabs({
</DialogHeader>
<div className="space-y-4 max-h-[600px] overflow-y-auto ">
- {data.map((group, groupIndex) => (
+ {filteredData.map((group, groupIndex) => (
<div key={groupIndex}>
{group.items.map((item) => {
const answerObj = form
diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx
index 1e172744..efb078e0 100644
--- a/components/pq-input/pq-review-wrapper.tsx
+++ b/components/pq-input/pq-review-wrapper.tsx
@@ -21,9 +21,10 @@ import {
DialogTitle
} from "@/components/ui/dialog"
import { useToast } from "@/hooks/use-toast"
-import { CheckCircle, AlertCircle, Paperclip, Square } from "lucide-react"
+import { CheckCircle, AlertCircle, Paperclip, Square, Download } from "lucide-react"
import { PQGroupData } from "@/lib/pq/service"
import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction } from "@/lib/pq/service"
+import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction, FileListIcon } from "@/components/ui/file-list"
// import * as ExcelJS from 'exceljs';
// import { saveAs } from "file-saver";
@@ -49,13 +50,15 @@ interface PQReviewWrapperProps {
vendorId: number
pqSubmission: PQSubmission
vendorInfo?: any // 협력업체 정보 (선택사항)
+ vendorCountry?: string | null
}
export function PQReviewWrapper({
pqData,
vendorId,
pqSubmission,
- vendorInfo
+ vendorInfo,
+ vendorCountry,
}: PQReviewWrapperProps) {
const router = useRouter()
const { toast } = useToast()
@@ -96,6 +99,32 @@ export function PQReviewWrapper({
return 0
})
}
+
+ // 벤더 내자/외자 판별 (국가 코드 기반)
+ const isDomesticVendor = React.useMemo(() => {
+ if (!vendorCountry) return null; // 정보 없으면 필터 미적용
+ return vendorCountry === "KR" || vendorCountry === "한국";
+ }, [vendorCountry]);
+
+ // 벤더 유형에 따라 PQ 항목 필터링
+ const filteredData: PQGroupData[] = React.useMemo(() => {
+ if (isDomesticVendor === null) return pqData;
+
+ const filterItemByType = (item: any) => {
+ const itemType = item.type || "내외자";
+ if (itemType === "내외자") return true;
+ if (itemType === "내자") return isDomesticVendor === true;
+ if (itemType === "외자") return isDomesticVendor === false;
+ return true;
+ };
+
+ return pqData
+ .map((group) => ({
+ ...group,
+ items: group.items.filter(filterItemByType),
+ }))
+ .filter((group) => group.items.length > 0);
+ }, [pqData, isDomesticVendor]);
// 기존 SHI 코멘트를 로컬 상태에 초기화
@@ -482,8 +511,14 @@ export function PQReviewWrapper({
return (
<div className="space-y-6">
+ {filteredData.length === 0 && (
+ <div className="rounded-md border border-dashed p-4 text-sm text-muted-foreground">
+ 표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용)
+ </div>
+ )}
+
{/* 그룹별 PQ 항목 표시 */}
- {pqData.map((group) => (
+ {filteredData.map((group) => (
<div key={group.groupName} className="space-y-4">
<h3 className="text-lg font-medium">{group.groupName}</h3>
@@ -530,6 +565,43 @@ export function PQReviewWrapper({
</div>
</CardHeader>
<CardContent className="space-y-4">
+ {item.criteriaAttachments && item.criteriaAttachments.length > 0 && (
+ <div className="space-y-2">
+ <p className="text-sm font-medium">기준 첨부파일</p>
+ <FileList>
+ {item.criteriaAttachments.map((file) => (
+ <FileListItem key={file.attachId}>
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{file.fileName}</FileListName>
+ {file.fileSize && (
+ <FileListDescription>{file.fileSize} bytes</FileListDescription>
+ )}
+ </FileListInfo>
+ <FileListAction
+ onClick={async () => {
+ try {
+ const { downloadFile } = await import('@/lib/file-download')
+ await downloadFile(file.filePath, file.fileName, { showToast: true })
+ } catch (error) {
+ toast({
+ title: "다운로드 실패",
+ description: "파일 다운로드 중 오류가 발생했습니다.",
+ variant: "destructive"
+ })
+ }
+ }}
+ >
+ <Download className="h-4 w-4" />
+ <span className="sr-only">Download</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ </div>
+ )}
{/* 프로젝트별 추가 정보 */}
{pqSubmission.projectId && item.contractInfo && (
<div className="space-y-1">