diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
| commit | b54f6f03150dd78d86db62201b6386bf14b72394 (patch) | |
| tree | b3092bb34805fdc65eee5282e86a9fb90ba20d6e /app | |
| parent | c1bd1a2f499ee2f0742170021b37dab410983ab7 (diff) | |
(대표님) 커버, 데이터룸, 파일매니저, 담당자할당 등
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(eng)/cover/page.tsx | 77 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(procurement)/po/page.tsx | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/files/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/files/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/layout.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/members/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/members/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/settings/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/settings/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/stats/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/[projectId]/stats/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/layout.tsx | 17 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/owner-companies/[id]/page.tsx | 43 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/owner-companies/[id]/users/new/page.tsx | 41 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/owner-companies/[id]/users/page.tsx | 63 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/owner-companies/new/page.tsx | 18 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/owner-companies/page.tsx | 32 | ||||
| -rw-r--r-- | app/[lng]/evcp/data-room/page.tsx (renamed from app/[lng]/evcp/(evcp)/data-room/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/po/page.tsx | 2 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/files/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/files/page.tsx) | 6 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/layout.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/members/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/members/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/settings/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/settings/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/stats/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/[projectId]/stats/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/layout.tsx | 17 | ||||
| -rw-r--r-- | app/[lng]/partners/data-room/page.tsx (renamed from app/[lng]/partners/(partners)/data-room/page.tsx) | 0 | ||||
| -rw-r--r-- | app/api/projects/[projectId]/cover/route.ts | 73 | ||||
| -rw-r--r-- | app/api/projects/cover-template/save/route.ts | 125 | ||||
| -rw-r--r-- | app/api/projects/cover-template/upload/route.ts | 127 |
27 files changed, 636 insertions, 7 deletions
diff --git a/app/[lng]/evcp/(evcp)/(eng)/cover/page.tsx b/app/[lng]/evcp/(evcp)/(eng)/cover/page.tsx new file mode 100644 index 00000000..9f2b2e61 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(eng)/cover/page.tsx @@ -0,0 +1,77 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsProjectsCache } from "@/lib/projects/validation" +import { InformationButton } from "@/components/information/information-button" +import { getProjectListsForCover } from "@/lib/cover/service" +import { ProjectsTableForCover } from "@/lib/cover/table/projects-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectListsForCover({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 프로젝트 리스트 + </h2> + <InformationButton pagePath="evcp/projects" /> + </div> + {/* <p className="text-muted-foreground"> + S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. + </p> */} + </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 + /> + } + > + <ProjectsTableForCover promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/po/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/po/page.tsx index 7479df8c..292ef1cb 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/po/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/po/page.tsx @@ -34,7 +34,7 @@ export default async function VendorPONew(props: VendorPOPageProps) { <div> <div className="flex items-center gap-2"> <h2 className="text-2xl font-bold tracking-tight"> - PO 관리 + PO/계약 관리 </h2> <InformationButton pagePath="evcp/po-new" /> </div> diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/files/page.tsx b/app/[lng]/evcp/data-room/[projectId]/files/page.tsx index baac96ad..baac96ad 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/files/page.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/files/page.tsx diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/layout.tsx b/app/[lng]/evcp/data-room/[projectId]/layout.tsx index d2e74f8e..d2e74f8e 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/layout.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/layout.tsx diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/members/page.tsx b/app/[lng]/evcp/data-room/[projectId]/members/page.tsx index 18442c0e..18442c0e 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/members/page.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/members/page.tsx diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/page.tsx b/app/[lng]/evcp/data-room/[projectId]/page.tsx index d54a8cab..d54a8cab 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/page.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/page.tsx diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/settings/page.tsx b/app/[lng]/evcp/data-room/[projectId]/settings/page.tsx index aa0f3b52..aa0f3b52 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/settings/page.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/settings/page.tsx diff --git a/app/[lng]/evcp/(evcp)/data-room/[projectId]/stats/page.tsx b/app/[lng]/evcp/data-room/[projectId]/stats/page.tsx index 7f652a99..7f652a99 100644 --- a/app/[lng]/evcp/(evcp)/data-room/[projectId]/stats/page.tsx +++ b/app/[lng]/evcp/data-room/[projectId]/stats/page.tsx diff --git a/app/[lng]/evcp/data-room/layout.tsx b/app/[lng]/evcp/data-room/layout.tsx new file mode 100644 index 00000000..9bef6027 --- /dev/null +++ b/app/[lng]/evcp/data-room/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { SiteFooter } from '@/components/layout/Footer'; +import { HeaderDataRoom } from '@/components/layout/HeaderDataroom'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( + <div className="flex flex-col h-screen bg-background"> + <HeaderDataRoom /> + <main className="flex-1 overflow-hidden"> + <div className='container-wrapper h-full'> + {children} + </div> + </main> + <SiteFooter/> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/data-room/owner-companies/[id]/page.tsx b/app/[lng]/evcp/data-room/owner-companies/[id]/page.tsx new file mode 100644 index 00000000..cc1901e4 --- /dev/null +++ b/app/[lng]/evcp/data-room/owner-companies/[id]/page.tsx @@ -0,0 +1,43 @@ +// app/evcp/data-room/owner-companies/[id]/page.tsx +import db from "@/db/db"; +import { ownerCompanies } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { notFound } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { OwnerCompanyForm } from "@/lib/owner-companies/owner-company-form"; + +export default async function EditOwnerCompanyPage({ + params, +}: { + params: { id: string }; +}) { + const companyId = parseInt(params.id); + + const [company] = await db + .select() + .from(ownerCompanies) + .where(eq(ownerCompanies.id, companyId)) + .limit(1); + + if (!company) { + notFound(); + } + + return ( + <div className="container mx-auto py-8 max-w-2xl"> + <Card> + <CardHeader> + <CardTitle>발주처 회사 정보 수정</CardTitle> + </CardHeader> + <CardContent> + <OwnerCompanyForm + initialData={{ + id: company.id, + name: company.name, + }} + /> + </CardContent> + </Card> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/data-room/owner-companies/[id]/users/new/page.tsx b/app/[lng]/evcp/data-room/owner-companies/[id]/users/new/page.tsx new file mode 100644 index 00000000..f78794c1 --- /dev/null +++ b/app/[lng]/evcp/data-room/owner-companies/[id]/users/new/page.tsx @@ -0,0 +1,41 @@ +// app/evcp/data-room/owner-companies/[id]/users/new/page.tsx +import db from "@/db/db"; +import { ownerCompanies } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { notFound } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { OwnerCompanyUserForm } from "@/lib/owner-companies/owner-company-user-form"; + +export default async function NewOwnerCompanyUserPage({ + params, +}: { + params: { id: string }; +}) { + const companyId = parseInt(params.id); + + const [company] = await db + .select() + .from(ownerCompanies) + .where(eq(ownerCompanies.id, companyId)) + .limit(1); + + if (!company) { + notFound(); + } + + return ( + <div className="container mx-auto py-8 max-w-2xl"> + <Card> + <CardHeader> + <CardTitle>{company.name} - 사용자 추가</CardTitle> + <CardDescription> + 발주처 사용자를 등록합니다. + </CardDescription> + </CardHeader> + <CardContent> + <OwnerCompanyUserForm companyId={companyId} /> + </CardContent> + </Card> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/data-room/owner-companies/[id]/users/page.tsx b/app/[lng]/evcp/data-room/owner-companies/[id]/users/page.tsx new file mode 100644 index 00000000..87ebb364 --- /dev/null +++ b/app/[lng]/evcp/data-room/owner-companies/[id]/users/page.tsx @@ -0,0 +1,63 @@ +// app/(admin)/owner-companies/[id]/users/page.tsx +import db from "@/db/db"; +import { users, ownerCompanies } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowLeft, Plus } from "lucide-react"; +import { OwnerCompanyUserList } from "@/lib/owner-companies/owner-company-user-list"; + +export default async function OwnerCompanyUsersPage({ + params, + }: { + params: { id: string }; + }) { + const companyId = parseInt(params.id); + + const [company] = await db + .select() + .from(ownerCompanies) + .where(eq(ownerCompanies.id, companyId)) + .limit(1); + + if (!company) { + notFound(); + } + + const companyUsers = await db + .select() + .from(users) + .where(eq(users.ownerCompanyId, companyId)) + .orderBy(users.createdAt); + + return ( + <div className="container mx-auto py-8"> + <div className="mb-4"> + <Button variant="ghost" size="sm" asChild> + <Link href="/evcp/data-room/owner-companies"> + <ArrowLeft className="h-4 w-4 mr-2" /> + 발주처 목록으로 + </Link> + </Button> + </div> + + <div className="flex justify-between items-center mb-6"> + <div> + <h1 className="text-3xl font-bold">{company.name}</h1> + <p className="text-muted-foreground mt-1"> + 사용자 관리 · 총 {companyUsers.length}명 + </p> + </div> + <Button asChild> + <Link href={`/evcp/data-room/owner-companies/${companyId}/users/new`}> + <Plus className="h-4 w-4 mr-2" /> + 사용자 추가 + </Link> + </Button> + </div> + + <OwnerCompanyUserList users={companyUsers} companyId={companyId} /> + </div> + ); + }
\ No newline at end of file diff --git a/app/[lng]/evcp/data-room/owner-companies/new/page.tsx b/app/[lng]/evcp/data-room/owner-companies/new/page.tsx new file mode 100644 index 00000000..166b8d41 --- /dev/null +++ b/app/[lng]/evcp/data-room/owner-companies/new/page.tsx @@ -0,0 +1,18 @@ +// app/(admin)/owner-companies/new/page.tsx +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { OwnerCompanyForm } from "@/lib/owner-companies/owner-company-form"; + +export default function NewOwnerCompanyPage() { + return ( + <div className="container mx-auto py-8 max-w-2xl"> + <Card> + <CardHeader> + <CardTitle>발주처 회사 등록</CardTitle> + </CardHeader> + <CardContent> + <OwnerCompanyForm /> + </CardContent> + </Card> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/data-room/owner-companies/page.tsx b/app/[lng]/evcp/data-room/owner-companies/page.tsx new file mode 100644 index 00000000..483d58bf --- /dev/null +++ b/app/[lng]/evcp/data-room/owner-companies/page.tsx @@ -0,0 +1,32 @@ +// app/evcp/data-room/owner-companies/page.tsx +import db from "@/db/db"; +import { ownerCompanies } from "@/db/schema"; +import { Button } from "@/components/ui/button"; +import { OwnerCompanyList } from "@/lib/owner-companies/owner-company-list"; +import Link from "next/link"; +import { Plus } from "lucide-react"; + +export default async function OwnerCompaniesPage() { + const companies = await db.select().from(ownerCompanies).orderBy(ownerCompanies.createdAt); + + return ( + <div className="container mx-auto py-8"> + <div className="flex justify-between items-center mb-6"> + <div> + <h1 className="text-3xl font-bold">발주처 관리</h1> + <p className="text-muted-foreground mt-1"> + 발주처 회사 및 사용자를 관리합니다 + </p> + </div> + <Button asChild> + <Link href="/evcp/data-room/owner-companies/new"> + <Plus className="h-4 w-4 mr-2" /> + 회사 등록 + </Link> + </Button> + </div> + + <OwnerCompanyList companies={companies} /> + </div> + ); + }
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/data-room/page.tsx b/app/[lng]/evcp/data-room/page.tsx index 4ff56abc..4ff56abc 100644 --- a/app/[lng]/evcp/(evcp)/data-room/page.tsx +++ b/app/[lng]/evcp/data-room/page.tsx diff --git a/app/[lng]/partners/(partners)/po/page.tsx b/app/[lng]/partners/(partners)/po/page.tsx index ebe7601e..c21d5e35 100644 --- a/app/[lng]/partners/(partners)/po/page.tsx +++ b/app/[lng]/partners/(partners)/po/page.tsx @@ -48,7 +48,7 @@ export default async function VendorPO(props: VendorPOPageProps) { <div> <div className="flex items-center gap-2"> <h2 className="text-2xl font-bold tracking-tight"> - 벤더 PO 관리 + PO/계약 목록 </h2> <InformationButton pagePath="partners/po" /> </div> diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/files/page.tsx b/app/[lng]/partners/data-room/[projectId]/files/page.tsx index 985e7fef..7197a2a7 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/files/page.tsx +++ b/app/[lng]/partners/data-room/[projectId]/files/page.tsx @@ -6,9 +6,5 @@ export default function ProjectFilesPage({ }: { params: { projectId: string }; }) { - return ( - <div className="h-full flex flex-col"> - <FileManager projectId={params.projectId} /> - </div> - ); + return <FileManager projectId={params.projectId} />; }
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/layout.tsx b/app/[lng]/partners/data-room/[projectId]/layout.tsx index d2e74f8e..d2e74f8e 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/layout.tsx +++ b/app/[lng]/partners/data-room/[projectId]/layout.tsx diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/members/page.tsx b/app/[lng]/partners/data-room/[projectId]/members/page.tsx index 18442c0e..18442c0e 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/members/page.tsx +++ b/app/[lng]/partners/data-room/[projectId]/members/page.tsx diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/page.tsx b/app/[lng]/partners/data-room/[projectId]/page.tsx index d54a8cab..d54a8cab 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/page.tsx +++ b/app/[lng]/partners/data-room/[projectId]/page.tsx diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/settings/page.tsx b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx index aa0f3b52..aa0f3b52 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/settings/page.tsx +++ b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx diff --git a/app/[lng]/partners/(partners)/data-room/[projectId]/stats/page.tsx b/app/[lng]/partners/data-room/[projectId]/stats/page.tsx index 7f652a99..7f652a99 100644 --- a/app/[lng]/partners/(partners)/data-room/[projectId]/stats/page.tsx +++ b/app/[lng]/partners/data-room/[projectId]/stats/page.tsx diff --git a/app/[lng]/partners/data-room/layout.tsx b/app/[lng]/partners/data-room/layout.tsx new file mode 100644 index 00000000..48913bd9 --- /dev/null +++ b/app/[lng]/partners/data-room/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { SiteFooter } from '@/components/layout/Footer'; +import { HeaderSimple } from '@/components/layout/HeaderSimple'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( + <div className="flex flex-col h-screen bg-background"> + <HeaderSimple /> + <main className="flex-1 overflow-hidden"> + <div className='container-wrapper h-full'> + {children} + </div> + </main> + <SiteFooter/> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/data-room/page.tsx b/app/[lng]/partners/data-room/page.tsx index 4ff56abc..4ff56abc 100644 --- a/app/[lng]/partners/(partners)/data-room/page.tsx +++ b/app/[lng]/partners/data-room/page.tsx diff --git a/app/api/projects/[projectId]/cover/route.ts b/app/api/projects/[projectId]/cover/route.ts new file mode 100644 index 00000000..b88f06ee --- /dev/null +++ b/app/api/projects/[projectId]/cover/route.ts @@ -0,0 +1,73 @@ +// app/api/projects/[projectId]/cover/route.ts +import { NextRequest, NextResponse } from "next/server" +import db from "@/db/db" +import { projectCoverTemplates, generatedCoverPages } from "@/db/schema" +import { eq, and, desc } from "drizzle-orm" + +export async function GET( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const projectId = parseInt(params.projectId) + + if (isNaN(projectId)) { + return NextResponse.json( + { success: false, message: "유효하지 않은 프로젝트 ID입니다" }, + { status: 400 } + ) + } + + // 1. 해당 프로젝트의 활성 템플릿 찾기 + const [activeTemplate] = await db + .select() + .from(projectCoverTemplates) + .where( + and( + eq(projectCoverTemplates.projectId, projectId), + eq(projectCoverTemplates.isActive, true) + ) + ) + .limit(1) + + if (!activeTemplate) { + return NextResponse.json( + { success: false, message: "활성 템플릿을 찾을 수 없습니다" }, + { status: 404 } + ) + } + + // 2. 해당 템플릿의 최신 생성된 커버 페이지 찾기 + const [latestCover] = await db + .select() + .from(generatedCoverPages) + .where(eq(generatedCoverPages.templateId, activeTemplate.id)) + .orderBy(desc(generatedCoverPages.generatedAt)) + .limit(1) + + if (!latestCover) { + return NextResponse.json( + { success: false, message: "생성된 커버 페이지를 찾을 수 없습니다" }, + { status: 404 } + ) + } + + // 3. 파일 경로와 정보 반환 + return NextResponse.json({ + success: true, + fileUrl: latestCover.filePath, + fileName: latestCover.fileName, + generatedAt: latestCover.generatedAt, + }) + + } catch (error) { + console.error("❌ 커버 페이지 조회 오류:", error) + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "조회 중 오류 발생" + }, + { status: 500 } + ) + } +}
\ No newline at end of file diff --git a/app/api/projects/cover-template/save/route.ts b/app/api/projects/cover-template/save/route.ts new file mode 100644 index 00000000..e681512d --- /dev/null +++ b/app/api/projects/cover-template/save/route.ts @@ -0,0 +1,125 @@ +// app/api/projects/cover-template/save/route.ts +import { saveFile } from "@/lib/file-stroage" +import db from "@/db/db" +import { projectCoverTemplates, generatedCoverPages } from "@/db/schema" +import { eq, and, desc } from "drizzle-orm" +import { NextRequest, NextResponse } from "next/server" +import { revalidateTag } from "next/cache" +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +export async function POST(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: '인증이 필요합니다' }, + { status: 401 } + ); + } + + const formData = await request.formData() + const file = formData.get("file") as File + const projectId = formData.get("projectId") as string + const templateName = formData.get("templateName") as string | null + + if (!file) { + return NextResponse.json( + { success: false, message: "파일이 없습니다" }, + { status: 400 } + ) + } + + if (!projectId) { + return NextResponse.json( + { success: false, message: "프로젝트 ID가 없습니다" }, + { status: 400 } + ) + } + + // 해당 프로젝트의 활성 템플릿 찾기 + const [activeTemplate] = await db + .select() + .from(projectCoverTemplates) + .where( + and( + eq(projectCoverTemplates.projectId, parseInt(projectId)), + eq(projectCoverTemplates.isActive, true) + ) + ) + .limit(1) + + if (!activeTemplate) { + return NextResponse.json( + { success: false, message: "활성 템플릿을 찾을 수 없습니다" }, + { status: 404 } + ) + } + + // 생성된 커버 페이지 저장 디렉토리 + const coverPagesDirectory = `projects/${projectId}/generated-covers` + + // 파일명 생성 (타임스탬프 포함) + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) + const fileName = templateName + ? `${templateName}_${timestamp}.docx` + : `cover_${timestamp}.docx` + + // 파일 저장 + const saveResult = await saveFile({ + file, + directory: coverPagesDirectory, + originalName: fileName, + }) + + if (!saveResult.success) { + return NextResponse.json( + { success: false, message: saveResult.error || "파일 저장 실패" }, + { status: 500 } + ) + } + + // TODO: 실제로는 문서에서 변수 값을 추출하거나 별도로 전달받아야 함 + // 현재는 빈 객체로 저장 (추후 확장 가능) + const variableValues = {} + + // generatedCoverPages 테이블에 저장 + const [generatedCover] = await db + .insert(generatedCoverPages) + .values({ + templateId: activeTemplate.id, + variableValues: variableValues, + fileName: saveResult.fileName, + filePath: saveResult.publicPath, + fileSize: saveResult.fileSize, + generatedBy: session.user.name, + }) + .returning() + + console.log(`✅ 커버 페이지 생성 완료: ${saveResult.fileName}`) + console.log(`✅ DB 저장 완료 - Generated Cover ID: ${generatedCover.id}`) + + // 캐시 무효화 + revalidateTag("project-cover-lists") + + return NextResponse.json({ + success: true, + generatedCoverId: generatedCover.id, + templateId: activeTemplate.id, + filePath: saveResult.publicPath, + fileName: saveResult.fileName, + fileSize: saveResult.fileSize, + message: "커버 페이지가 저장되었습니다" + }) + + } catch (error) { + console.error("❌ 커버 페이지 저장 오류:", error) + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "저장 중 오류 발생" + }, + { status: 500 } + ) + } +}
\ No newline at end of file diff --git a/app/api/projects/cover-template/upload/route.ts b/app/api/projects/cover-template/upload/route.ts new file mode 100644 index 00000000..9c8df7ca --- /dev/null +++ b/app/api/projects/cover-template/upload/route.ts @@ -0,0 +1,127 @@ +// app/api/projects/cover-template/upload/route.ts +import db from "@/db/db" +import { projectCoverTemplates } from "@/db/schema" +import { saveFile } from "@/lib/file-stroage" +import { eq, and } from "drizzle-orm" +import { NextRequest, NextResponse } from "next/server" +import { revalidateTag } from "next/cache" +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +export async function POST(request: NextRequest) { + try { + + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: '인증이 필요합니다' }, + { status: 401 } + ); + } + + const formData = await request.formData() + const file = formData.get("file") as File + const projectId = formData.get("projectId") as string + + if (!file) { + return NextResponse.json( + { success: false, message: "파일이 없습니다" }, + { status: 400 } + ) + } + + if (!projectId) { + return NextResponse.json( + { success: false, message: "프로젝트 ID가 없습니다" }, + { status: 400 } + ) + } + + // 파일 확장자 확인 + if (!file.name.endsWith('.docx')) { + return NextResponse.json( + { success: false, message: "DOCX 파일만 업로드 가능합니다" }, + { status: 400 } + ) + } + + // 템플릿 디렉토리 + const templateDirectory = `projects/${projectId}/cover-templates` + + // 파일 저장 + const saveResult = await saveFile({ + file, + directory: templateDirectory, + originalName: file.name, + }) + + if (!saveResult.success) { + return NextResponse.json( + { success: false, message: saveResult.error || "파일 저장 실패" }, + { status: 500 } + ) + } + + // 기존 활성 템플릿 비활성화 + await db + .update(projectCoverTemplates) + .set({ + isActive: false, + updatedAt: new Date() + }) + .where( + and( + eq(projectCoverTemplates.projectId, parseInt(projectId)), + eq(projectCoverTemplates.isActive, true) + ) + ) + + // 기본 템플릿 변수 설정 + const defaultVariables = { + docNumber: "{{docNumber}}", + projectNumber: "{{projectNumber}}", + projectName: "{{projectName}}" + } + + // 새 템플릿을 DB에 저장 + const [newTemplate] = await db + .insert(projectCoverTemplates) + .values({ + projectId: parseInt(projectId), + templateName: file.name.replace('.docx', ''), + originalFileName: file.name, + filePath: saveResult.publicPath, + fileSize: saveResult.fileSize, + variables: defaultVariables, + isActive: true, + createdBy: session.user.name, // TODO: 실제 사용자 정보로 변경 + updatedBy: session.user.name, + }) + .returning() + + console.log(`✅ 커버 템플릿 업로드 완료: ${saveResult.fileName}`) + console.log(`✅ DB 저장 완료 - Template ID: ${newTemplate.id}`) + + // 캐시 무효화 + revalidateTag("project-cover-lists") + + return NextResponse.json({ + success: true, + templateId: newTemplate.id, + filePath: saveResult.publicPath, + fileName: saveResult.fileName, + fileSize: saveResult.fileSize, + variables: defaultVariables, + }) + + } catch (error) { + console.error("❌ 템플릿 업로드 오류:", error) + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "업로드 중 오류 발생" + }, + { status: 500 } + ) + } +}
\ No newline at end of file |
