diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/evaluation/page.tsx | 8 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/legal-review/page.tsx | 87 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/polices/page.tsx | 238 | ||||
| -rw-r--r-- | app/[lng]/partners/signup/page.tsx | 2 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts | 2 |
5 files changed, 329 insertions, 8 deletions
diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx index ae626e58..0d3848d9 100644 --- a/app/[lng]/evcp/(evcp)/evaluation/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx @@ -136,17 +136,13 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations // ✅ nuqs 기반 파라미터 파싱 const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) // 현재 평가년도 const currentEvaluationYear = search.evaluationYear // ✅ 집계 모드를 지원하는 서비스 함수 사용 const promises = Promise.all([ - getPeriodicEvaluationsWithAggregation({ - ...search, - filters: validFilters, - }) + getPeriodicEvaluationsWithAggregation(search) ]) // ✅ 현재 모드 표시용 변수 @@ -193,7 +189,7 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations {/* 메인 테이블 */} <React.Suspense - key={JSON.stringify(searchParams)} // 집계 모드 변경 시에도 리렌더링 + // key={JSON.stringify(searchParams)} // 집계 모드 변경 시에도 리렌더링 fallback={ <DataTableSkeleton columnCount={search.aggregated ? 17 : 15} // 집계 모드에서 컬럼 추가 diff --git a/app/[lng]/evcp/(evcp)/legal-review/page.tsx b/app/[lng]/evcp/(evcp)/legal-review/page.tsx new file mode 100644 index 00000000..63560db3 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/legal-review/page.tsx @@ -0,0 +1,87 @@ +// app/(routes)/legal-works/page.tsx 수정 + +import * as React from "react"; +import { Metadata } from "next"; +import { type SearchParams } from "@/types/table"; +import { Shell } from "@/components/shell"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { InformationButton } from "@/components/information/information-button"; +import { Badge } from "@/components/ui/badge"; // ✅ Badge 추가 +import { SearchParamsCacheLegalWorks } from "@/lib/legal-review/validations"; +import { getLegalWorks } from "@/lib/legal-review/service"; +import { LegalWorksTable } from "@/lib/legal-review/status/legal-table"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export const metadata: Metadata = { + title: "법무업무 관리", + description: "법무 검토 요청 및 답변을 관리합니다.", +}; + +interface LegalWorksPageProps { + searchParams: Promise<SearchParams>; +} + +export default async function LegalWorksPage({ searchParams }: LegalWorksPageProps) { + const rawParams = await searchParams; + const parsedSearch = SearchParamsCacheLegalWorks.parse(rawParams); + + // ✅ EvaluationTargetsPage와 동일한 패턴으로 currentYear 추가 + const currentYear = new Date().getFullYear(); + + const promises = Promise.all([ + getLegalWorks(parsedSearch) + ]); + + return ( + <Shell className="gap-4"> + {/* Header - EvaluationTargetsPage와 동일한 패턴 */} + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight">법무업무 관리</h2> + <InformationButton pagePath="evcp/legal-review" /> + {/* ✅ EvaluationTargetsPage와 동일하게 Badge 추가 */} + <Badge variant="outline" className="text-sm"> + {currentYear}년 + </Badge> + </div> + </div> + + {/* Table */} + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={13} + searchableColumnCount={3} + filterableColumnCount={4} + cellWidths={[ + "3rem", // checkbox + "4rem", // No. + "5rem", // 구분 + "6rem", // 상태 + "8rem", // Vendor Code + "12rem", // Vendor Name + "4rem", // 긴급여부 + "7rem", // 답변요청일 + "7rem", // 의뢰일 + "7rem", // 답변예정일 + "7rem", // 법무완료일 + "8rem", // 검토요청자 + "8rem", // 법무답변자 + "4rem", // 첨부파일 + "8rem", // actions + ]} + shrinkZero + /> + } + > + {/* ✅ currentYear prop 추가 - EvaluationTargetsTable과 동일한 패턴 */} + <LegalWorksTable + promises={promises} + currentYear={currentYear} + /> + </React.Suspense> + </Shell> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/polices/page.tsx b/app/[lng]/evcp/(evcp)/polices/page.tsx new file mode 100644 index 00000000..46a9e87a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/polices/page.tsx @@ -0,0 +1,238 @@ +// app/admin/policies/page.tsx (서버 컴포넌트) +import { Suspense } from 'react' +import { Metadata } from 'next' +import { eq, desc } from 'drizzle-orm' +import db from '@/db/db' +import { policyVersions } from '@/db/schema' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { FileText, Shield, Calendar, User, Clock } from 'lucide-react' +import { PolicyManagementClient } from '@/components/polices/policy-management-client' + +export const metadata: Metadata = { + title: '정책 관리 | eVCP Admin', + description: '개인정보 처리방침 및 이용약관 관리' +} + +// 정책 데이터 조회 함수 +async function getPoliciesData() { + try { + // 현재 활성 정책들 + const currentPolicies = await db + .select() + .from(policyVersions) + .where(eq(policyVersions.isCurrent, true)) + .orderBy(policyVersions.policyType) + + // 전체 정책 히스토리 + const allPolicies = await db + .select() + .from(policyVersions) + .orderBy(desc(policyVersions.createdAt)) + + // 정책 타입별로 그룹화 + const policiesByType = { + privacy_policy: allPolicies.filter(p => p.policyType === 'privacy_policy'), + terms_of_service: allPolicies.filter(p => p.policyType === 'terms_of_service') + } + + // 현재 정책 맵 + const currentPolicyMap = {} + currentPolicies.forEach(policy => { + currentPolicyMap[policy.policyType] = policy + }) + + return { + currentPolicies: currentPolicyMap, + allPolicies: policiesByType, + stats: { + totalVersions: allPolicies.length, + privacyVersions: policiesByType.privacy_policy.length, + termsVersions: policiesByType.terms_of_service.length, + lastUpdate: allPolicies[0]?.createdAt || null + } + } + } catch (error) { + console.error('Failed to fetch policies:', error) + return { + currentPolicies: {}, + allPolicies: { privacy_policy: [], terms_of_service: [] }, + stats: { totalVersions: 0, privacyVersions: 0, termsVersions: 0, lastUpdate: null } + } + } +} + +export default async function PoliciesPage() { + const data = await getPoliciesData() + + return ( + <div className="container mx-auto py-6 space-y-6"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div> + <h1 className="text-3xl font-bold tracking-tight">정책 관리</h1> + <p className="text-muted-foreground"> + 개인정보 처리방침과 이용약관을 버전별로 관리합니다 + </p> + </div> + </div> + + {/* 통계 카드들 */} + <div className="grid gap-4 md:grid-cols-4"> + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">총 버전 수</CardTitle> + <FileText className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.totalVersions}</div> + <p className="text-xs text-muted-foreground"> + 전체 정책 버전 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">개인정보 정책</CardTitle> + <Shield className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.privacyVersions}</div> + <p className="text-xs text-muted-foreground"> + 버전 수 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">이용약관</CardTitle> + <FileText className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{data.stats.termsVersions}</div> + <p className="text-xs text-muted-foreground"> + 버전 수 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">최근 업데이트</CardTitle> + <Clock className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {data.stats.lastUpdate + ? new Date(data.stats.lastUpdate).toLocaleDateString('ko-KR') + : 'N/A' + } + </div> + <p className="text-xs text-muted-foreground"> + 마지막 정책 변경 + </p> + </CardContent> + </Card> + </div> + + {/* 현재 활성 정책들 */} + <div className="grid gap-6 md:grid-cols-2"> + <CurrentPolicyCard + title="개인정보 처리방침" + icon={<Shield className="h-5 w-5" />} + policy={data.currentPolicies.privacy_policy} + type="privacy_policy" + /> + <CurrentPolicyCard + title="이용약관" + icon={<FileText className="h-5 w-5" />} + policy={data.currentPolicies.terms_of_service} + type="terms_of_service" + /> + </div> + + <Separator /> + + {/* 클라이언트 컴포넌트로 편집 기능 제공 */} + <Suspense fallback={<PolicyManagementSkeleton />}> + <PolicyManagementClient initialData={data} /> + </Suspense> + </div> + ) +} + +// 현재 정책 카드 컴포넌트 +function CurrentPolicyCard({ title, icon, policy, type }) { + if (!policy) { + return ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + {icon} + {title} + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-8 text-muted-foreground"> + <p>아직 등록된 정책이 없습니다</p> + <p className="text-sm mt-2">새 버전을 생성해주세요</p> + </div> + </CardContent> + </Card> + ) + } + + return ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + {icon} + {title} + <Badge variant="secondary">v{policy.version}</Badge> + </CardTitle> + <CardDescription> + 현재 활성 정책 • 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-3"> + {/* 정책 내용 미리보기 */} + <div className="bg-muted/50 p-3 rounded-md text-sm max-h-32 overflow-hidden"> + <div className="line-clamp-4"> + {policy.content?.replace(/#{1,6}\s+/g, '').replace(/\*\*(.*?)\*\*/g, '$1').substring(0, 200)}... + </div> + </div> + + {/* 메타 정보 */} + <div className="flex items-center justify-between text-xs text-muted-foreground"> + <div className="flex items-center gap-1"> + <Calendar className="h-3 w-3" /> + 생성: {new Date(policy.createdAt).toLocaleDateString('ko-KR')} + </div> + <div className="flex items-center gap-1"> + <User className="h-3 w-3" /> + 관리자 + </div> + </div> + </div> + </CardContent> + </Card> + ) +} + +// 로딩 스켈레톤 +function PolicyManagementSkeleton() { + return ( + <div className="space-y-4"> + <div className="h-8 bg-muted animate-pulse rounded" /> + <div className="grid gap-4 md:grid-cols-2"> + <div className="h-32 bg-muted animate-pulse rounded" /> + <div className="h-32 bg-muted animate-pulse rounded" /> + </div> + <div className="h-96 bg-muted animate-pulse rounded" /> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/signup/page.tsx b/app/[lng]/partners/signup/page.tsx index 26c2944b..551cad14 100644 --- a/app/[lng]/partners/signup/page.tsx +++ b/app/[lng]/partners/signup/page.tsx @@ -1,7 +1,7 @@ import { Suspense } from "react" import { Metadata } from "next" -import { JoinForm } from "@/components/signup/join-form" import { JoinFormSkeleton } from "@/components/signup/join-form-skeleton" +import JoinForm from "@/components/signup/join-form" // (Optional) If Next.js attempts to statically optimize this page and you need full runtime // behavior for query params, you may also need: diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts index f0915527..c855d168 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts @@ -76,7 +76,7 @@ export async function POST(request: NextRequest) { return withSoapLogging( 'INBOUND', - 'MDG + 'MDG', 'IF_MDZ_EVCP_MODEL_MASTER', body, async () => { |
