diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
| commit | 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch) | |
| tree | f406b5c86f563347c7fd088a85fd1a82284dc5ff /app | |
| parent | 6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff) | |
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/engineering/(engineering)/report/page.tsx | 154 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/report/page.tsx | 4 | ||||
| -rw-r--r-- | app/[lng]/page.tsx | 158 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/report/page.tsx | 154 | ||||
| -rw-r--r-- | app/[lng]/sales/(sales)/report/page.tsx | 154 | ||||
| -rw-r--r-- | app/api/richtext/route.ts | 54 | ||||
| -rw-r--r-- | app/api/vendor-responses/update-comment/route.ts | 2 | ||||
| -rw-r--r-- | app/api/vendor-responses/update/route.ts | 1 |
8 files changed, 571 insertions, 110 deletions
diff --git a/app/[lng]/engineering/(engineering)/report/page.tsx b/app/[lng]/engineering/(engineering)/report/page.tsx index 3efaa7c3..eb932e0f 100644 --- a/app/[lng]/engineering/(engineering)/report/page.tsx +++ b/app/[lng]/engineering/(engineering)/report/page.tsx @@ -1,47 +1,129 @@ -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" +// app/procurement/dashboard/page.tsx +import * as React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Shell } from "@/components/shell"; +import { ErrorBoundary } from "@/components/error-boundary"; +import { getDashboardData } from "@/lib/dashboard/service"; +import { DashboardClient } from "@/lib/dashboard/dashboard-client"; +// 대시보드 데이터 로딩 컴포넌트 +async function DashboardContent() { + try { + const data = await getDashboardData("engineering"); + + const handleRefresh = async () => { + "use server"; + return await getDashboardData("engineering"); + }; -export default async function IndexPage() { - + return ( + <DashboardClient + initialData={data} + onRefresh={handleRefresh} + /> + ); + } catch (error) { + console.error("Dashboard data loading error:", error); + throw error; + } +} +// 대시보드 로딩 스켈레톤 +function DashboardSkeleton() { return ( - <Shell className="gap-2"> + <div className="space-y-6"> + {/* 헤더 스켈레톤 */} <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Dashboard - </h2> - <p className="text-muted-foreground"> - 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. - </p> + <div className="space-y-2"> + <Skeleton className="h-8 w-48" /> + <Skeleton className="h-4 w-72" /> + </div> + <Skeleton className="h-10 w-24" /> + </div> + + {/* 요약 카드 스켈레톤 */} + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> + {[...Array(4)].map((_, i) => ( + <div key={i} className="space-y-3 p-6 border rounded-lg"> + <div className="flex items-center justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-4" /> + </div> + <Skeleton className="h-8 w-12" /> + <Skeleton className="h-3 w-20" /> + </div> + ))} + </div> + + {/* 차트 스켈레톤 */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {[...Array(2)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <div className="space-y-2"> + <Skeleton className="h-6 w-32" /> + <Skeleton className="h-4 w-48" /> + </div> + <Skeleton className="h-[300px] w-full" /> + </div> + ))} + </div> + + {/* 탭 스켈레톤 */} + <div className="space-y-4"> + <Skeleton className="h-10 w-64" /> + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {[...Array(6)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <Skeleton className="h-6 w-32" /> + <div className="space-y-3"> + <div className="flex justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-12" /> + </div> + <div className="flex gap-2"> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + </div> + <Skeleton className="h-2 w-full" /> + </div> + </div> + ))} </div> </div> + </div> + ); +} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } +// 에러 표시 컴포넌트 +function DashboardError({ error, reset }: { error: Error; reset: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-12 space-y-4"> + <div className="text-center space-y-2"> + <h3 className="text-lg font-semibold">대시보드를 불러올 수 없습니다</h3> + <p className="text-muted-foreground"> + {error.message || "알 수 없는 오류가 발생했습니다."} + </p> + </div> + <button + onClick={reset} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" > - </React.Suspense> + 다시 시도 + </button> + </div> + ); +} + +export default async function DashboardPage() { + return ( + <Shell className="gap-6"> + <ErrorBoundary fallback={DashboardError}> + <React.Suspense fallback={<DashboardSkeleton />}> + <DashboardContent /> + </React.Suspense> + </ErrorBoundary> </Shell> - ) -}
\ No newline at end of file + ); +} diff --git a/app/[lng]/evcp/(evcp)/report/page.tsx b/app/[lng]/evcp/(evcp)/report/page.tsx index eeb6a91d..95566b05 100644 --- a/app/[lng]/evcp/(evcp)/report/page.tsx +++ b/app/[lng]/evcp/(evcp)/report/page.tsx @@ -10,11 +10,11 @@ import { DashboardClient } from "@/lib/dashboard/dashboard-client"; // 대시보드 데이터 로딩 컴포넌트 async function DashboardContent() { try { - const data = await getDashboardData("procurement"); + const data = await getDashboardData("evcp"); const handleRefresh = async () => { "use server"; - return await getDashboardData("procurement"); + return await getDashboardData("evcp"); }; return ( diff --git a/app/[lng]/page.tsx b/app/[lng]/page.tsx new file mode 100644 index 00000000..2ee83857 --- /dev/null +++ b/app/[lng]/page.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import Link from 'next/link'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ShoppingCart, Users, Settings, ArrowRight, Building2 } from 'lucide-react'; + +export default function LandingPage() { + const portals = [ + { + id: 'sales', + title: '기술영업포탈', + description: '기술 영업 단계에서의 RFQ를 관리할 수 있는 통합 플랫폼', + icon: Users, + color: 'from-emerald-500 to-teal-500', + href: '/sales', + features: ['벤더 관리', '기술 영업 RFQ'] + }, + { + id: 'purchase', + title: '구매포탈', + description: '협력업체에서부터 마지막 발주까지 원스톱 구매 솔루션', + icon: ShoppingCart, + color: 'from-blue-500 to-cyan-500', + href: '/procurement', + features: ['협력업체 관리', '구매관리'] + }, + + { + id: 'design', + title: '설계포탈', + description: '벤더가 플랫폼을 통해 데이터와 문서를 제출할 수 있게 하고 TBE를 처리할 수 있는 플랫폼', + icon: Settings, + color: 'from-purple-500 to-pink-500', + href: '/engineering', + features: ['설계 기준정보관리', 'TBE'] + } + ]; + + + + return ( + <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> + {/* Header */} + <header className="relative overflow-hidden"> + <div className="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-purple-600/10"></div> + <div className="relative container mx-auto px-4 py-16 text-center"> + <div className="flex items-center justify-center mb-6"> + <Building2 className="h-12 w-12 text-blue-600 mr-3" /> + <h1 className="text-4xl md:text-6xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> + enterprise Vendor Co-work Platform + </h1> + </div> + <p className="text-xl md:text-2xl text-slate-600 dark:text-slate-300 max-w-3xl mx-auto leading-relaxed"> + 통합된 비즈니스 솔루션으로 구매부터 설계까지, + <br />모든 업무 프로세스를 하나의 플랫폼에서 관리하세요 + </p> + <Badge variant="secondary" className="mt-6 px-4 py-2 text-sm"> + Enterprise Ready + </Badge> + </div> + </header> + + {/* Main Portal Selection */} + <main className="container mx-auto px-4 py-16"> + <div className="text-center mb-16"> + <h2 className="text-3xl md:text-4xl font-bold text-slate-800 dark:text-slate-100 mb-4"> + 포털을 선택하세요 + </h2> + <p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto"> + 각 포털은 특화된 기능과 도구를 제공하여 업무 효율성을 극대화합니다 + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto"> + {portals.map((portal) => { + const Icon = portal.icon; + return ( + <Link key={portal.id} href={portal.href} className="block"> + <Card className="relative group cursor-pointer transition-all duration-300 hover:scale-105 hover:shadow-2xl border-0 bg-white dark:bg-slate-800 overflow-hidden h-full"> + <div className={`absolute inset-0 bg-gradient-to-br ${portal.color} opacity-5 group-hover:opacity-10 transition-opacity duration-300`}></div> + + <CardHeader className="relative pb-4"> + <div className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${portal.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}> + <Icon className="h-8 w-8 text-white" /> + </div> + <CardTitle className="text-2xl font-bold text-slate-800 dark:text-slate-100 group-hover:text-transparent group-hover:bg-gradient-to-r group-hover:from-blue-600 group-hover:to-purple-600 group-hover:bg-clip-text transition-all duration-300"> + {portal.title} + </CardTitle> + <CardDescription className="text-slate-600 dark:text-slate-400 text-base leading-relaxed"> + {portal.description} + </CardDescription> + </CardHeader> + + <CardContent className="relative"> + <div className="mb-6"> + <h4 className="font-semibold text-slate-700 dark:text-slate-300 mb-3">주요 기능</h4> + <div className="space-y-2"> + {portal.features.map((feature, idx) => ( + <div key={idx} className="flex items-center text-sm text-slate-600 dark:text-slate-400"> + <div className={`w-2 h-2 rounded-full bg-gradient-to-r ${portal.color} mr-3`}></div> + {feature} + </div> + ))} + </div> + </div> + + <Button className={`w-full bg-gradient-to-r ${portal.color} hover:opacity-90 text-white border-0 group-hover:shadow-lg transition-all duration-300`}> + 포털 접속하기 + <ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform duration-300" /> + </Button> + </CardContent> + </Card> + </Link> + ); + })} + </div> + + {/* Additional Info Section */} + <div className="mt-20 text-center"> + <div className="bg-white dark:bg-slate-800 rounded-3xl p-8 md:p-12 shadow-xl border border-slate-200 dark:border-slate-700 max-w-4xl mx-auto"> + <h3 className="text-2xl md:text-3xl font-bold text-slate-800 dark:text-slate-100 mb-4"> + 모든 포털이 연동됩니다 + </h3> + <p className="text-lg text-slate-600 dark:text-slate-400 mb-8"> + 구매, 영업, 설계 포털 간의 데이터가 실시간으로 동기화되어 + 효율적인 업무 협업이 가능합니다 + </p> + <div className="flex flex-wrap justify-center gap-4"> + <Badge variant="outline" className="px-4 py-2">실시간 동기화</Badge> + <Badge variant="outline" className="px-4 py-2">통합 대시보드</Badge> + <Badge variant="outline" className="px-4 py-2">권한 관리</Badge> + <Badge variant="outline" className="px-4 py-2">보안 인증</Badge> + </div> + </div> + </div> + </main> + + {/* Footer */} + <footer className="bg-slate-800 dark:bg-slate-900 text-white py-12 mt-20"> + <div className="container mx-auto px-4 text-center"> + <div className="flex items-center justify-center mb-6"> + <Building2 className="h-8 w-8 mr-2" /> + <span className="text-xl font-semibold">enterprise Vendor Co-work Platform</span> + </div> + <p className="text-slate-400 mb-4"> + © 2025 삼성중공업. All rights reserved. + </p> + {/* <div className="flex justify-center space-x-6 text-sm text-slate-400"> + <Link href="/terms" className="hover:text-white transition-colors">이용약관</Link> + <Link href="/privacy" className="hover:text-white transition-colors">개인정보처리방침</Link> + <Link href="/support" className="hover:text-white transition-colors">고객지원</Link> + </div> */} + </div> + </footer> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/report/page.tsx b/app/[lng]/procurement/(procurement)/report/page.tsx index 3efaa7c3..800fbd8b 100644 --- a/app/[lng]/procurement/(procurement)/report/page.tsx +++ b/app/[lng]/procurement/(procurement)/report/page.tsx @@ -1,47 +1,129 @@ -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" +// app/procurement/dashboard/page.tsx +import * as React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Shell } from "@/components/shell"; +import { ErrorBoundary } from "@/components/error-boundary"; +import { getDashboardData } from "@/lib/dashboard/service"; +import { DashboardClient } from "@/lib/dashboard/dashboard-client"; +// 대시보드 데이터 로딩 컴포넌트 +async function DashboardContent() { + try { + const data = await getDashboardData("procurement"); + + const handleRefresh = async () => { + "use server"; + return await getDashboardData("procurement"); + }; -export default async function IndexPage() { - + return ( + <DashboardClient + initialData={data} + onRefresh={handleRefresh} + /> + ); + } catch (error) { + console.error("Dashboard data loading error:", error); + throw error; + } +} +// 대시보드 로딩 스켈레톤 +function DashboardSkeleton() { return ( - <Shell className="gap-2"> + <div className="space-y-6"> + {/* 헤더 스켈레톤 */} <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Dashboard - </h2> - <p className="text-muted-foreground"> - 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. - </p> + <div className="space-y-2"> + <Skeleton className="h-8 w-48" /> + <Skeleton className="h-4 w-72" /> + </div> + <Skeleton className="h-10 w-24" /> + </div> + + {/* 요약 카드 스켈레톤 */} + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> + {[...Array(4)].map((_, i) => ( + <div key={i} className="space-y-3 p-6 border rounded-lg"> + <div className="flex items-center justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-4" /> + </div> + <Skeleton className="h-8 w-12" /> + <Skeleton className="h-3 w-20" /> + </div> + ))} + </div> + + {/* 차트 스켈레톤 */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {[...Array(2)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <div className="space-y-2"> + <Skeleton className="h-6 w-32" /> + <Skeleton className="h-4 w-48" /> + </div> + <Skeleton className="h-[300px] w-full" /> + </div> + ))} + </div> + + {/* 탭 스켈레톤 */} + <div className="space-y-4"> + <Skeleton className="h-10 w-64" /> + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {[...Array(6)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <Skeleton className="h-6 w-32" /> + <div className="space-y-3"> + <div className="flex justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-12" /> + </div> + <div className="flex gap-2"> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + </div> + <Skeleton className="h-2 w-full" /> + </div> + </div> + ))} </div> </div> + </div> + ); +} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } +// 에러 표시 컴포넌트 +function DashboardError({ error, reset }: { error: Error; reset: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-12 space-y-4"> + <div className="text-center space-y-2"> + <h3 className="text-lg font-semibold">대시보드를 불러올 수 없습니다</h3> + <p className="text-muted-foreground"> + {error.message || "알 수 없는 오류가 발생했습니다."} + </p> + </div> + <button + onClick={reset} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" > - </React.Suspense> + 다시 시도 + </button> + </div> + ); +} + +export default async function DashboardPage() { + return ( + <Shell className="gap-6"> + <ErrorBoundary fallback={DashboardError}> + <React.Suspense fallback={<DashboardSkeleton />}> + <DashboardContent /> + </React.Suspense> + </ErrorBoundary> </Shell> - ) -}
\ No newline at end of file + ); +} diff --git a/app/[lng]/sales/(sales)/report/page.tsx b/app/[lng]/sales/(sales)/report/page.tsx index 3efaa7c3..33225e33 100644 --- a/app/[lng]/sales/(sales)/report/page.tsx +++ b/app/[lng]/sales/(sales)/report/page.tsx @@ -1,47 +1,129 @@ -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" +// app/procurement/dashboard/page.tsx +import * as React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Shell } from "@/components/shell"; +import { ErrorBoundary } from "@/components/error-boundary"; +import { getDashboardData } from "@/lib/dashboard/service"; +import { DashboardClient } from "@/lib/dashboard/dashboard-client"; +// 대시보드 데이터 로딩 컴포넌트 +async function DashboardContent() { + try { + const data = await getDashboardData("sales"); + + const handleRefresh = async () => { + "use server"; + return await getDashboardData("sales"); + }; -export default async function IndexPage() { - + return ( + <DashboardClient + initialData={data} + onRefresh={handleRefresh} + /> + ); + } catch (error) { + console.error("Dashboard data loading error:", error); + throw error; + } +} +// 대시보드 로딩 스켈레톤 +function DashboardSkeleton() { return ( - <Shell className="gap-2"> + <div className="space-y-6"> + {/* 헤더 스켈레톤 */} <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Dashboard - </h2> - <p className="text-muted-foreground"> - 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. - </p> + <div className="space-y-2"> + <Skeleton className="h-8 w-48" /> + <Skeleton className="h-4 w-72" /> + </div> + <Skeleton className="h-10 w-24" /> + </div> + + {/* 요약 카드 스켈레톤 */} + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> + {[...Array(4)].map((_, i) => ( + <div key={i} className="space-y-3 p-6 border rounded-lg"> + <div className="flex items-center justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-4" /> + </div> + <Skeleton className="h-8 w-12" /> + <Skeleton className="h-3 w-20" /> + </div> + ))} + </div> + + {/* 차트 스켈레톤 */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {[...Array(2)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <div className="space-y-2"> + <Skeleton className="h-6 w-32" /> + <Skeleton className="h-4 w-48" /> + </div> + <Skeleton className="h-[300px] w-full" /> + </div> + ))} + </div> + + {/* 탭 스켈레톤 */} + <div className="space-y-4"> + <Skeleton className="h-10 w-64" /> + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {[...Array(6)].map((_, i) => ( + <div key={i} className="space-y-4 p-6 border rounded-lg"> + <Skeleton className="h-6 w-32" /> + <div className="space-y-3"> + <div className="flex justify-between"> + <Skeleton className="h-4 w-16" /> + <Skeleton className="h-4 w-12" /> + </div> + <div className="flex gap-2"> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + <Skeleton className="h-6 w-16" /> + </div> + <Skeleton className="h-2 w-full" /> + </div> + </div> + ))} </div> </div> + </div> + ); +} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } +// 에러 표시 컴포넌트 +function DashboardError({ error, reset }: { error: Error; reset: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-12 space-y-4"> + <div className="text-center space-y-2"> + <h3 className="text-lg font-semibold">대시보드를 불러올 수 없습니다</h3> + <p className="text-muted-foreground"> + {error.message || "알 수 없는 오류가 발생했습니다."} + </p> + </div> + <button + onClick={reset} + className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" > - </React.Suspense> + 다시 시도 + </button> + </div> + ); +} + +export default async function DashboardPage() { + return ( + <Shell className="gap-6"> + <ErrorBoundary fallback={DashboardError}> + <React.Suspense fallback={<DashboardSkeleton />}> + <DashboardContent /> + </React.Suspense> + </ErrorBoundary> </Shell> - ) -}
\ No newline at end of file + ); +} diff --git a/app/api/richtext/route.ts b/app/api/richtext/route.ts new file mode 100644 index 00000000..3d050dc5 --- /dev/null +++ b/app/api/richtext/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { randomUUID } from 'crypto'; + +export async function POST(req: NextRequest) { + try { + const formData = await req.formData(); + const file = formData.get('file') as File; + + console.log(file) + + if (!file) { + return NextResponse.json({ error: '파일이 없습니다.' }, { status: 400 }); + } + + // 파일 확장자 검증 + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; + if (!allowedTypes.includes(file.type)) { + return NextResponse.json({ error: '지원하지 않는 파일 형식입니다.' }, { status: 400 }); + } + + // 파일 크기 제한 (5MB) + if (file.size > 5 * 1024 * 1024) { + return NextResponse.json({ error: '파일 크기는 5MB 이하여야 합니다.' }, { status: 400 }); + } + + // 업로드 디렉토리 생성 + const uploadDir = join(process.cwd(), 'public', 'uploads', 'richtext'); + await mkdir(uploadDir, { recursive: true }); + + // 고유한 파일명 생성 + const fileExtension = file.name.split('.').pop(); + const fileName = `${randomUUID()}.${fileExtension}`; + const filePath = join(uploadDir, fileName); + + // 파일 저장 + const arrayBuffer = await file.arrayBuffer(); + await writeFile(filePath, new Uint8Array(arrayBuffer)); + + // 공개 URL 반환 + const fileUrl = `/uploads/richtext/${fileName}`; + + return NextResponse.json({ + success: true, + url: fileUrl, + message: '이미지가 성공적으로 업로드되었습니다.' + }); + + } catch (error) { + console.error('파일 업로드 실패:', error); + return NextResponse.json({ error: '파일 업로드에 실패했습니다.' }, { status: 500 }); + } +}
\ No newline at end of file diff --git a/app/api/vendor-responses/update-comment/route.ts b/app/api/vendor-responses/update-comment/route.ts index 212173d7..f1e4c487 100644 --- a/app/api/vendor-responses/update-comment/route.ts +++ b/app/api/vendor-responses/update-comment/route.ts @@ -5,6 +5,7 @@ import { vendorAttachmentResponses } from "@/db/schema"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { eq } from "drizzle-orm"; export async function POST(request: NextRequest) { try { @@ -34,6 +35,7 @@ export async function POST(request: NextRequest) { responseComment, vendorComment, updatedAt: new Date(), + updatedBy:Number(session?.user.id) }) .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) .returning(); diff --git a/app/api/vendor-responses/update/route.ts b/app/api/vendor-responses/update/route.ts index 8771b062..5ee31d4d 100644 --- a/app/api/vendor-responses/update/route.ts +++ b/app/api/vendor-responses/update/route.ts @@ -94,6 +94,7 @@ export async function POST(request: NextRequest) { vendorComment, respondedAt: respondedAt ? new Date(respondedAt) : null, updatedAt: new Date(), + updatedBy:Number(session?.user.id) }) .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) .returning(); |
