From b84621f9b2b7161a5ad4f0b194264e9df3e65dbf Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 8 Jul 2025 11:23:40 +0000 Subject: (대표님) 20250708 미반영분 커밋 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx | 52 +++++++ app/[lng]/evcp/(evcp)/system/roles/page.tsx | 2 +- app/[lng]/page.tsx | 181 ++++++++++++++---------- app/api/auth/[...nextauth]/route.ts | 62 +++++--- app/api/vendor-responses/update/route.ts | 14 +- 5 files changed, 206 insertions(+), 105 deletions(-) (limited to 'app') diff --git a/app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx b/app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx index e69de29b..d50ec03d 100644 --- a/app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx +++ b/app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx @@ -0,0 +1,52 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getFinalRfqDetail } from "@/lib/b-rfq/service" +import { searchParamsFinalRfqDetailCache } from "@/lib/b-rfq/validations" +import { FinalRfqDetailTable } from "@/lib/b-rfq/final/final-rfq-detail-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsFinalRfqDetailCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getFinalRfqDetail({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ Fianl RFQ List +

+

+ 업체에게 최종 RFQ를 송부하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/system/roles/page.tsx b/app/[lng]/evcp/(evcp)/system/roles/page.tsx index fe074600..9f3a4dd8 100644 --- a/app/[lng]/evcp/(evcp)/system/roles/page.tsx +++ b/app/[lng]/evcp/(evcp)/system/roles/page.tsx @@ -56,7 +56,7 @@ export default async function UserTable(props: IndexPageProps) {

Role Management

- 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. + 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. 구매를 위해서는 정기평자 담당자, 실사 담당자가 지정되어있어야합니다.

diff --git a/app/[lng]/page.tsx b/app/[lng]/page.tsx index 2ee83857..d0018f40 100644 --- a/app/[lng]/page.tsx +++ b/app/[lng]/page.tsx @@ -3,112 +3,119 @@ 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'; +import { Briefcase, Package, Settings, ArrowRight, CheckCircle, Monitor, Shield, BarChart3, Building2 } from 'lucide-react'; export default function LandingPage() { const portals = [ { id: 'sales', - title: '기술영업포탈', + title: '기술 영업 포털', description: '기술 영업 단계에서의 RFQ를 관리할 수 있는 통합 플랫폼', - icon: Users, - color: 'from-emerald-500 to-teal-500', + icon: Briefcase, + color: 'from-green-500 to-emerald-500', href: '/sales', - features: ['벤더 관리', '기술 영업 RFQ'] + features: ['벤더 관리', '기술 영업 RFQ'], }, { id: 'purchase', - title: '구매포탈', - description: '협력업체에서부터 마지막 발주까지 원스톱 구매 솔루션', - icon: ShoppingCart, - color: 'from-blue-500 to-cyan-500', + title: '구매 포털', + description: '협력 업체에서 부터 마지막 발주까지 원스톱 구매 솔루션', + icon: Package, + color: 'from-blue-500 to-indigo-500', href: '/procurement', - features: ['협력업체 관리', '구매관리'] + features: ['협력 업체 관리', '구매 관리'], }, - { id: 'design', - title: '설계포탈', + title: '설계 포털', description: '벤더가 플랫폼을 통해 데이터와 문서를 제출할 수 있게 하고 TBE를 처리할 수 있는 플랫폼', icon: Settings, - color: 'from-purple-500 to-pink-500', + color: 'from-purple-500 to-violet-500', href: '/engineering', - features: ['설계 기준정보관리', 'TBE'] + features: ['설계 기준정보 관리', 'TBE'], } ]; - - return ( -
+
{/* Header */}
-
-
-
- -

- enterprise Vendor Co-work Platform +
+
+
+
+

+ Enterprise Vendor Co-work Platform

-

+

통합된 비즈니스 솔루션으로 구매부터 설계까지, -
모든 업무 프로세스를 하나의 플랫폼에서 관리하세요 +
+ 모든 업무 프로세스를 하나의 플랫폼에서 관리하세요

- - Enterprise Ready -

{/* Main Portal Selection */} -
-
-

+
+
+
+
+

포털을 선택하세요

-

+

각 포털은 특화된 기능과 도구를 제공하여 업무 효율성을 극대화합니다

+
-
+
{portals.map((portal) => { const Icon = portal.icon; return ( - -
+ +
- -
- + +
+
+ +
- + {portal.title} - + {portal.description}
- +
-

주요 기능

+

주요 기능

{portal.features.map((feature, idx) => ( -
-
- {feature} +
+
+ {feature}
))}
- +
+ +
@@ -117,42 +124,68 @@ export default function LandingPage() {
{/* Additional Info Section */} -
-
-

- 모든 포털이 연동됩니다 -

-

- 구매, 영업, 설계 포털 간의 데이터가 실시간으로 동기화되어 - 효율적인 업무 협업이 가능합니다 -

-
- 실시간 동기화 - 통합 대시보드 - 권한 관리 - 보안 인증 +
+ {/* Left Section - Text Content */} +
+
+

+ 모든 포털이 연동됩니다 +

+

+ 구매, 영업, 설계 포털 간의 데이터가 +
+ 실시간으로 동기화되어 효율적인 업무 협업이 가능합니다 +

+ + {/* Right Section - Feature Icons */} +
+
+
+ + 실시간 동기화 +
+
+ + 통합 대시보드 +
+
+ + 권한 관리 +
+
+ + 보안 인증 +
+
+
+
- + {/* Footer */} -

+ ); } \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index e059377c..68cc3a5b 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -13,15 +13,18 @@ import { verifyOtpTemp } from '@/lib/users/verifyOtp' import { getSecuritySettings } from '@/lib/password-policy/service' import { verifySmsToken } from '@/lib/users/auth/passwordUtil' import { SessionRepository } from '@/lib/users/session/repository' +import { getUserRoles } from '@/lib/users/service' + // 인증 방식 타입 정의 type AuthMethod = 'otp' | 'email' | 'sgips' | 'saml' -// 모듈 보강 선언 - ID를 string으로 통일 + +// ✅ 모듈 보강 선언 - roles 배열 추가 declare module "next-auth" { interface Session { user: { - id: string // number → string으로 변경 + id: string name?: string | null email?: string | null image?: string | null @@ -32,11 +35,12 @@ declare module "next-auth" { authMethod?: AuthMethod sessionExpiredAt?: number | null dbSessionId?: string | null + roles?: string[] // ✅ roles 배열 추가 } } interface User { - id: string // number → string으로 변경 + id: string imageUrl?: string | null companyId?: number | null techCompanyId?: number | null @@ -44,12 +48,13 @@ declare module "next-auth" { reAuthTime?: number | null authMethod?: AuthMethod dbSessionId?: string | null + roles?: string[] // ✅ roles 배열 추가 } } declare module "next-auth/jwt" { interface JWT { - id?: string // 이미 string이므로 그대로 + id?: string imageUrl?: string | null companyId?: number | null techCompanyId?: number | null @@ -58,6 +63,7 @@ declare module "next-auth/jwt" { authMethod?: AuthMethod sessionExpiredAt?: number | null dbSessionId?: string | null + roles?: string[] // ✅ roles 배열 추가 } } @@ -118,7 +124,7 @@ function getClientIP(req: any): string { export const authOptions: NextAuthOptions = { providers: [ - // OTP 로그인 - 타입 에러 수정 + // ✅ OTP 로그인 - roles 정보 추가 CredentialsProvider({ id: 'credentials-otp', name: 'OTP', @@ -134,12 +140,14 @@ export const authOptions: NextAuthOptions = { return null } + // ✅ 사용자 roles 정보 조회 + const userRoles = await getUserRoles(user.id) + const securitySettings = await getCachedSecuritySettings() const reAuthTime = Date.now() - // 반환 객체의 id를 string으로 변환 return { - id: ensureString(user.id), // ✅ string으로 변환 + id: ensureString(user.id), email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, @@ -148,16 +156,17 @@ export const authOptions: NextAuthOptions = { domain: user.domain, reAuthTime, authMethod: 'otp' as AuthMethod, + roles: userRoles, // ✅ roles 배열 추가 } }, }), - // MFA 완료 후 최종 인증 - 타입 에러 수정 + // ✅ MFA 완료 후 최종 인증 - roles 정보 추가 CredentialsProvider({ id: 'credentials-mfa', name: 'MFA Verification', credentials: { - userId: { label: 'User ID', type: 'text' }, // number → text로 변경 + userId: { label: 'User ID', type: 'text' }, smsToken: { label: 'SMS Token', type: 'text' }, tempAuthKey: { label: 'Temp Auth Key', type: 'text' }, }, @@ -167,7 +176,6 @@ export const authOptions: NextAuthOptions = { return null } - // userId를 number로 변환하여 DB 조회 const numericUserId = ensureNumber(credentials.userId) const user = await getUserById(numericUserId) if (!user) { @@ -193,6 +201,9 @@ export const authOptions: NextAuthOptions = { // 임시 인증 정보를 사용됨으로 표시 await SessionRepository.markTempAuthSessionAsUsed(credentials.tempAuthKey) + // ✅ 사용자 roles 정보 조회 + const userRoles = await getUserRoles(user.id) + // 보안 설정 및 세션 정보 설정 const securitySettings = await getCachedSecuritySettings() const reAuthTime = Date.now() @@ -203,7 +214,7 @@ export const authOptions: NextAuthOptions = { const userAgent = req.headers?.['user-agent'] const dbSession = await SessionRepository.createLoginSession({ - userId: user.id, // number로 전달 + userId: user.id, ipAddress, userAgent, authMethod: tempAuth.authMethod, @@ -212,9 +223,8 @@ export const authOptions: NextAuthOptions = { console.log(`MFA completed for user ${user.email} (${tempAuth.authMethod})`) - // 반환 객체의 id를 string으로 변환 return { - id: ensureString(user.id), // ✅ string으로 변환 + id: ensureString(user.id), email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, @@ -224,6 +234,7 @@ export const authOptions: NextAuthOptions = { reAuthTime, authMethod: tempAuth.authMethod as AuthMethod, dbSessionId: dbSession.id, + roles: userRoles, // ✅ roles 배열 추가 } } catch (error) { @@ -271,6 +282,7 @@ export const authOptions: NextAuthOptions = { }, callbacks: { + // ✅ JWT callback에 roles 정보 추가 async jwt({ token, user, account, trigger, session }) { const securitySettings = await getCachedSecuritySettings() const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000 @@ -278,7 +290,7 @@ export const authOptions: NextAuthOptions = { // 최초 로그인 시 (MFA 완료 후) if (user) { const reAuthTime = Date.now() - token.id = user.id // ✅ 이제 둘 다 string 타입 + token.id = user.id token.email = user.email token.name = user.name token.companyId = user.companyId @@ -289,16 +301,24 @@ export const authOptions: NextAuthOptions = { token.authMethod = user.authMethod token.sessionExpiredAt = reAuthTime + sessionTimeoutMs token.dbSessionId = user.dbSessionId + token.roles = user.roles // ✅ roles 정보 추가 } - // SAML 인증 시 DB 세션 생성 + // SAML 인증 시 DB 세션 생성 및 roles 조회 if (account && account.provider === 'credentials-saml' && token.id) { const reAuthTime = Date.now() const sessionExpiredAt = new Date(reAuthTime + sessionTimeoutMs) try { + const numericUserId = ensureNumber(token.id) + + // ✅ SAML 로그인 시에도 roles 정보 조회 + if (!token.roles) { + token.roles = await getUserRoles(numericUserId) + } + const dbSession = await SessionRepository.createLoginSession({ - userId: ensureNumber(token.id), // string을 number로 변환하여 DB에 저장 + userId: numericUserId, ipAddress: '0.0.0.0', authMethod: 'saml', sessionExpiredAt, @@ -338,6 +358,7 @@ export const authOptions: NextAuthOptions = { return token }, + // ✅ Session callback에 roles 정보 추가 async session({ session, token }: { session: Session; token: JWT }) { // 세션 만료 체크 if (token.sessionExpiredAt && Date.now() > token.sessionExpiredAt) { @@ -356,7 +377,7 @@ export const authOptions: NextAuthOptions = { if (token) { session.user = { - id: token.id as string, // ✅ string으로 일관성 유지 + id: token.id as string, email: token.email as string, name: token.name as string, domain: token.domain as string, @@ -367,6 +388,7 @@ export const authOptions: NextAuthOptions = { authMethod: token.authMethod as AuthMethod, sessionExpiredAt: token.sessionExpiredAt as number | null, dbSessionId: token.dbSessionId as string | null, + roles: token.roles as string[] || [], // ✅ roles 정보 추가 } } return session @@ -396,7 +418,7 @@ export const authOptions: NextAuthOptions = { // 이미 MFA에서 DB 세션이 생성된 경우가 아니라면 여기서 생성 if (account?.provider !== 'credentials-mfa' && user.id) { try { - const numericUserId = ensureNumber(user.id) // string을 number로 변환 + const numericUserId = ensureNumber(user.id) // 기존 활성 세션 확인 const existingSession = await SessionRepository.getActiveSessionByUserId(numericUserId) @@ -427,15 +449,13 @@ export const authOptions: NextAuthOptions = { await SessionRepository.logoutSession(dbSessionId) } else if (userId) { // dbSessionId가 없는 경우 사용자의 모든 활성 세션 로그아웃 - const numericUserId = ensureNumber(userId) // string을 number로 변환 + const numericUserId = ensureNumber(userId) await SessionRepository.logoutAllUserSessions(numericUserId) } } } } - const handler = NextAuth(authOptions) -// ✅ 핵심: 반드시 GET, POST를 named export로 내보내야 함 export { handler as GET, handler as POST } \ No newline at end of file diff --git a/app/api/vendor-responses/update/route.ts b/app/api/vendor-responses/update/route.ts index 5ee31d4d..cf7e551c 100644 --- a/app/api/vendor-responses/update/route.ts +++ b/app/api/vendor-responses/update/route.ts @@ -9,7 +9,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/route" // 리비전 번호를 증가시키는 헬퍼 함수 function getNextRevision(currentRevision?: string): string { if (!currentRevision) { - return "Rev.1"; // 첫 번째 응답 + return "Rev.0"; // 첫 번째 응답 } // "Rev.1" -> 1, "Rev.2" -> 2 형태로 숫자 추출 @@ -20,7 +20,7 @@ function getNextRevision(currentRevision?: string): string { } // 형식이 다르면 기본값 반환 - return "Rev.1"; + return "Rev.0"; } export async function POST(request: NextRequest) { @@ -68,17 +68,13 @@ export async function POST(request: NextRequest) { // 2. 벤더 응답 리비전 결정 let nextRespondedRevision: string; + if (responseStatus === "RESPONDED") { - // 새로운 응답이거나 수정 응답인 경우 리비전 증가 - if (currentResponse.responseStatus === "NOT_RESPONDED" || - currentResponse.responseStatus === "REVISION_REQUESTED") { + // 첫 응답이거나 수정 요청 후 재응답인 경우 리비전 증가 nextRespondedRevision = getNextRevision(currentResponse.respondedRevision); - } else { - // 이미 응답된 상태에서 다시 업데이트하는 경우 (코멘트 수정 등) - nextRespondedRevision = currentResponse.respondedRevision || "Rev.1"; - } + } else { // WAIVED 등 다른 상태는 기존 리비전 유지 nextRespondedRevision = currentResponse.respondedRevision || ""; -- cgit v1.2.3