summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/b-rfq/[id]/final/page.tsx52
-rw-r--r--app/[lng]/evcp/(evcp)/system/roles/page.tsx2
-rw-r--r--app/[lng]/page.tsx181
-rw-r--r--app/api/auth/[...nextauth]/route.ts62
-rw-r--r--app/api/vendor-responses/update/route.ts14
5 files changed, 206 insertions, 105 deletions
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<SearchParams>
+}
+
+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 (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">
+ Fianl RFQ List
+ </h3>
+ <p className="text-sm text-muted-foreground">
+ 업체에게 최종 RFQ를 송부하는 화면입니다.
+ </p>
+ </div>
+ <Separator />
+ <div>
+ <FinalRfqDetailTable promises={promises} rfqId={idAsNumber}/>
+ </div>
+ </div>
+ )
+} \ 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) {
<div>
<h3 className="text-lg font-medium">Role Management</h3>
<p className="text-sm text-muted-foreground">
- 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다.
+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. 구매를 위해서는 정기평자 담당자, 실사 담당자가 지정되어있어야합니다.
</p>
</div>
<Separator />
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 (
- <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
+ <div className="min-h-screen bg-white">
{/* 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
+ <div
+ className="absolute inset-0 bg-cover bg-center bg-no-repeat"
+ style={{
+ backgroundImage: "url('/images/headerImg.png')"
+ }}
+ ></div>
+ <div className="absolute inset-0 bg-black/20"></div>
+ <div className="relative container mx-auto px-4 py-16 md:py-24 text-center">
+ <div className="flex items-center justify-center mb-8">
+ <h1 className="text-3xl md:text-5xl lg:text-6xl font-bold text-white leading-tight">
+ 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">
+ <p className="text-lg md:text-xl lg:text-2xl text-white max-w-4xl mx-auto leading-relaxed mb-8">
통합된 비즈니스 솔루션으로 구매부터 설계까지,
- <br />모든 업무 프로세스를 하나의 플랫폼에서 관리하세요
+ <br className="hidden md:block" />
+ 모든 업무 프로세스를 하나의 플랫폼에서 관리하세요
</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">
+ <main className="container mx-auto px-4 py-12 md:py-20">
+ <div className="flex flex-col max-w-7xl mx-auto ">
+ <div className="flex flex-col items-left justify-center" >
+ <div className="text-left mb-12 md:mb-16">
+ <h2 className="text-2xl md:text-3xl lg:text-4xl font-bold text-slate-800 mb-4">
포털을 선택하세요
</h2>
- <p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
+ <p className="text-base md:text-lg text-slate-600 max-w-2xl leading-relaxed">
각 포털은 특화된 기능과 도구를 제공하여 업무 효율성을 극대화합니다
</p>
</div>
+ </div>
- <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md: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>
+ <Card className="relative group cursor-pointer transition-all duration-300 hover:scale-105 hover:shadow-xl bg-white border-slate-200 overflow-hidden h-full">
+ <div className={`absolute inset-0 bg-gradient-to-br ${portal.color} opacity-0 group-hover:opacity-5 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" />
+ <CardHeader className="relative pb-3">
+ <div className="flex items-start justify-between mb-4">
+ <div className={`w-12 h-12 md:w-14 md:h-14 rounded-lg bg-gradient-to-br ${portal.color} flex items-center justify-center group-hover:scale-110 transition-transform duration-300 flex-shrink-0`}>
+ <Icon className="h-6 w-6 md:h-7 md:w-7 text-white" />
+ </div>
</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">
+ <CardTitle className="text-xl md:text-2xl font-bold text-slate-800 text-left 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 mb-2">
{portal.title}
</CardTitle>
- <CardDescription className="text-slate-600 dark:text-slate-400 text-base leading-relaxed">
+ <CardDescription className="text-slate-600 text-sm md:text-base leading-relaxed text-left">
{portal.description}
</CardDescription>
</CardHeader>
- <CardContent className="relative">
+ <CardContent className="relative pt-0">
<div className="mb-6">
- <h4 className="font-semibold text-slate-700 dark:text-slate-300 mb-3">주요 기능</h4>
+ <h4 className="font-semibold text-slate-700 mb-3 text-sm md:text-base text-left">주요 기능</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 key={idx} className="flex items-center text-sm text-slate-600">
+ <div className={`w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-gradient-to-r ${portal.color} mr-3 flex-shrink-0`}></div>
+ <span className="text-xs md:text-sm text-left">{feature}</span>
</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>
+ <div className="flex justify-end">
+ <Button className={`bg-gradient-to-r ${portal.color} hover:opacity-90 text-white border-0 group-hover:shadow-lg transition-all duration-300 text-sm px-6 py-2`}>
+ 접속하기
+ <ArrowRight className="ml-2 h-3 w-3 md:h-4 md:w-4 group-hover:translate-x-1 transition-transform duration-300" />
+ </Button>
+ </div>
</CardContent>
</Card>
</Link>
@@ -117,42 +124,68 @@ export default function LandingPage() {
</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 className="mt-16 md:mt-20 flex flex-col md:flex-row gap-8 md:gap-12">
+ {/* Left Section - Text Content */}
+ <div className="flex-1 text-left">
+ <div className="bg-white mt-12 rounded-lg">
+ <h3 className="text-xl md:text-2xl lg:text-3xl font-bold text-slate-800 mb-4">
+ 모든 포털이 연동됩니다
+ </h3>
+ <p className="text-sm md:text-base lg:text-lg text-slate-600 mb-6 md:mb-8 leading-relaxed">
+ 구매, 영업, 설계 포털 간의 데이터가
+ <br className="hidden md:block" />
+ 실시간으로 동기화되어 효율적인 업무 협업이 가능합니다
+ </p>
</div>
</div>
+
+ {/* Right Section - Feature Icons */}
+ <div className="flex-1">
+ <div className="grid grid-cols-2 gap-3 md:gap-4">
+ <div className="flex flex-col items-center justify-center bg-slate-100 rounded-lg px-4 py-4 md:px-6 md:py-6">
+ <CheckCircle className="h-8 w-8 md:h-12 md:w-12 text-green-500 mb-2 flex-shrink-0" />
+ <span className="text-xs md:text-sm text-slate-700 text-center">실시간 동기화</span>
+ </div>
+ <div className="flex flex-col items-center justify-center bg-slate-100 rounded-lg px-4 py-4 md:px-6 md:py-6">
+ <Monitor className="h-8 w-8 md:h-12 md:w-12 text-blue-500 mb-2 flex-shrink-0" />
+ <span className="text-xs md:text-sm text-slate-700 text-center">통합 대시보드</span>
+ </div>
+ <div className="flex flex-col items-center justify-center bg-slate-100 rounded-lg px-4 py-4 md:px-6 md:py-6">
+ <Settings className="h-8 w-8 md:h-12 md:w-12 text-purple-500 mb-2 flex-shrink-0" />
+ <span className="text-xs md:text-sm text-slate-700 text-center">권한 관리</span>
+ </div>
+ <div className="flex flex-col items-center justify-center bg-slate-100 rounded-lg px-4 py-4 md:px-6 md:py-6">
+ <Shield className="h-8 w-8 md:h-12 md:w-12 text-yellow-500 mb-2 flex-shrink-0" />
+ <span className="text-xs md:text-sm text-slate-700 text-center">보안 인증</span>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</main>
-
+
{/* Footer */}
- <footer className="bg-slate-800 dark:bg-slate-900 text-white py-12 mt-20">
+ <footer className="bg-slate-800 text-white py-8 md:py-12 mt-16 md: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 className="flex flex-col items-center justify-center mb-4 md:mb-6">
+ <div className="flex items-center justify-center">
+ <img
+ src="/images/samsung_logo.png"
+ alt="삼성중공업 로고"
+ className="h-8 w-auto md:h-10 mr-2"
+ />
+ <span className="text-lg md:text-xl font-semibold">삼성중공업 <span className="font-normal">eVCP</span></span>
+ </div>
+ <div className="text-sm md:text-base text-slate-300 mt-2">
+ Enterprise Vendor Co-work Platform
+ </div>
</div>
- <p className="text-slate-400 mb-4">
+ <p className="text-slate-400 text-xs md:text-sm">
© 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/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 || "";