summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation/page.tsx8
-rw-r--r--app/[lng]/evcp/(evcp)/legal-review/page.tsx87
-rw-r--r--app/[lng]/evcp/(evcp)/polices/page.tsx238
-rw-r--r--app/[lng]/partners/signup/page.tsx2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts2
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 () => {