summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/api/auth/first-auth/route.ts46
-rw-r--r--components/login/login-form.tsx54
-rw-r--r--i18n/locales/en/login.json7
-rw-r--r--i18n/locales/ko/login.json7
-rw-r--r--lib/admin-users/service.ts20
-rw-r--r--lib/admin-users/table/add-ausers-dialog.tsx50
-rw-r--r--lib/admin-users/table/ausers-table.tsx5
-rw-r--r--lib/admin-users/table/update-auser-sheet.tsx56
-rw-r--r--lib/admin-users/validations.ts33
-rw-r--r--lib/users/auth/verifyCredentails.ts1
-rw-r--r--lib/users/session/helper.ts4
-rw-r--r--lib/vendor-users/service.ts3
12 files changed, 238 insertions, 48 deletions
diff --git a/app/api/auth/first-auth/route.ts b/app/api/auth/first-auth/route.ts
index ff92e71c..e8d86a02 100644
--- a/app/api/auth/first-auth/route.ts
+++ b/app/api/auth/first-auth/route.ts
@@ -18,6 +18,7 @@ interface FirstAuthResponse {
userId?: number
email?: string
error?: string
+ errorCode?: string
}
export async function POST(request: NextRequest): Promise<NextResponse<FirstAuthResponse>> {
@@ -63,19 +64,52 @@ export async function POST(request: NextRequest): Promise<NextResponse<FirstAuth
const authResult = await authHelpers.performFirstAuth(username, password, provider)
if (!authResult.success) {
- // 인증 실패 응답
+ // 인증 실패 응답 - 세분화된 에러 코드 처리
let errorMessage = '인증에 실패했습니다.'
+ let errorCode = 'AUTHENTICATION_FAILED'
- if (provider === 'sgips') {
- errorMessage = 'S-Gips 계정 정보가 올바르지 않습니다.'
- } else {
- errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.'
+ // authResult.error에서 세분화된 에러 타입 확인
+ if (authResult.error) {
+ switch (authResult.error) {
+ case 'INVALID_CREDENTIALS':
+ errorCode = 'INVALID_CREDENTIALS'
+ errorMessage = provider === 'sgips'
+ ? 'S-GIPS 계정 정보를 확인해주세요.'
+ : '이메일 또는 비밀번호를 확인해주세요.'
+ break
+ case 'ACCOUNT_LOCKED':
+ errorCode = 'ACCOUNT_LOCKED'
+ errorMessage = '계정이 일시적으로 잠겼습니다. 잠시 후 다시 시도해주세요.'
+ break
+ case 'ACCOUNT_DEACTIVATED':
+ errorCode = 'ACCOUNT_DEACTIVATED'
+ errorMessage = '비활성화된 계정입니다. 관리자에게 문의해주세요.'
+ break
+ case 'RATE_LIMITED':
+ errorCode = 'RATE_LIMITED'
+ errorMessage = '로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.'
+ break
+ case 'VENDOR_NOT_FOUND':
+ errorCode = 'VENDOR_NOT_FOUND'
+ errorMessage = '등록되지 않은 벤더입니다. 담당자에게 문의해주세요.'
+ break
+ case 'SYSTEM_ERROR':
+ errorCode = 'SYSTEM_ERROR'
+ errorMessage = '일시적인 시스템 오류입니다. 계속 문제가 발생하면 담당자에게 문의해주세요.'
+ break
+ default:
+ errorCode = 'AUTHENTICATION_FAILED'
+ errorMessage = provider === 'sgips'
+ ? 'S-Gips 인증에 실패했습니다.'
+ : '인증에 실패했습니다.'
+ }
}
return NextResponse.json(
{
success: false,
- error: authResult.error || errorMessage
+ error: errorMessage,
+ errorCode: errorCode
},
{ status: 401 }
)
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx
index f8ba21d9..7453edb6 100644
--- a/components/login/login-form.tsx
+++ b/components/login/login-form.tsx
@@ -9,7 +9,7 @@ import { useToast } from "@/hooks/use-toast";
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu"
import { useTranslation } from '@/i18n/client'
import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation';
-import { signIn, getSession } from 'next-auth/react';
+import { signIn } from 'next-auth/react';
import { buttonVariants } from "@/components/ui/button"
import Link from "next/link"
import Image from 'next/image';
@@ -23,10 +23,7 @@ import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth";
type LoginMethod = 'username' | 'sgips';
-export function LoginForm({
- className,
- ...props
-}: React.ComponentProps<"div">) {
+export function LoginForm() {
const params = useParams() || {};
const pathname = usePathname() || '';
const router = useRouter();
@@ -75,6 +72,32 @@ export function LoginForm({
const currentLanguageText = i18n.language === 'ko' ? t('languages.korean') : t('languages.english');
+ // 세분화된 에러 메시지 처리 함수
+ const getErrorMessage = (error: { errorCode?: string; message?: string }, provider: 'email' | 'sgips') => {
+ const errorCode = error.errorCode;
+
+ if (!errorCode) {
+ return error.message || t('authenticationFailed');
+ }
+
+ switch (errorCode) {
+ case 'INVALID_CREDENTIALS':
+ return provider === 'sgips' ? t('sgipsInvalidCredentials') : t('invalidCredentials');
+ case 'ACCOUNT_LOCKED':
+ return t('accountLocked');
+ case 'ACCOUNT_DEACTIVATED':
+ return t('accountDeactivated');
+ case 'RATE_LIMITED':
+ return t('rateLimited');
+ case 'VENDOR_NOT_FOUND':
+ return t('vendorNotFound');
+ case 'SYSTEM_ERROR':
+ return t('systemError');
+ default:
+ return error.message || t('authenticationFailed');
+ }
+ };
+
const goToVendorRegistration = () => {
router.push(`/${lng}/partners/repository`);
};
@@ -120,7 +143,10 @@ export function LoginForm({
const result = await response.json();
if (!response.ok) {
- throw new Error(result.error || t('authenticationFailed'));
+ // 세분화된 에러 메시지 처리
+ const error = new Error(result.error || t('authenticationFailed')) as Error & { errorCode?: string };
+ error.errorCode = result.errorCode;
+ throw error;
}
return result;
@@ -216,7 +242,7 @@ export function LoginForm({
const callbackUrl = new URL(callbackUrlParam);
const relativeUrl = callbackUrl.pathname + callbackUrl.search;
router.push(relativeUrl);
- } catch (e) {
+ } catch {
router.push(callbackUrlParam);
}
} else {
@@ -297,13 +323,10 @@ export function LoginForm({
description: t('sendingCodeToPhone'),
});
}
- } catch (error: any) {
+ } catch (error: unknown) {
console.error('Username login error:', error);
- let errorMessage = t('invalidCredentials');
- if (error.message) {
- errorMessage = error.message;
- }
+ const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'email');
toast({
title: t('errorTitle'),
@@ -356,13 +379,10 @@ export function LoginForm({
description: t('sendingCodeToSgipsPhone'),
});
}
- } catch (error: any) {
+ } catch (error: unknown) {
console.error('S-Gips login error:', error);
- let errorMessage = t('sgipsLoginFailed');
- if (error.message) {
- errorMessage = error.message;
- }
+ const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'sgips');
toast({
title: t('errorTitle'),
diff --git a/i18n/locales/en/login.json b/i18n/locales/en/login.json
index 674279f2..a05913b1 100644
--- a/i18n/locales/en/login.json
+++ b/i18n/locales/en/login.json
@@ -96,6 +96,13 @@
"mfaAuthError": "An error occurred during MFA authentication.",
"resetPasswordTitle": "Set New Password",
"resetPasswordDescription": "Please set a strong password for your account security.",
+ "invalidCredentials": "Please check your email or password.",
+ "sgipsInvalidCredentials": "Please check your S-GIPS account information.",
+ "accountLocked": "Account is temporarily locked. Please try again later.",
+ "accountDeactivated": "Account has been deactivated. Please contact administrator.",
+ "rateLimited": "Too many login attempts. Please try again later.",
+ "systemError": "Temporary system error. Please contact support if the problem persists.",
+ "vendorNotFound": "Vendor not found. Please contact administrator.",
"newPassword": "New Password",
"newPasswordPlaceholder": "Enter your new password",
"confirmPassword": "Confirm Password",
diff --git a/i18n/locales/ko/login.json b/i18n/locales/ko/login.json
index 9dec5c56..f2247c0e 100644
--- a/i18n/locales/ko/login.json
+++ b/i18n/locales/ko/login.json
@@ -96,6 +96,13 @@
"mfaAuthError": "MFA 인증 중 오류가 발생했습니다.",
"resetPasswordTitle": "새 비밀번호 설정",
"resetPasswordDescription": "계정 보안을 위해 강력한 비밀번호를 설정해주세요.",
+ "invalidCredentials": "이메일 또는 비밀번호를 확인해주세요.",
+ "sgipsInvalidCredentials": "S-GIPS 계정 정보를 확인해주세요.",
+ "accountLocked": "계정이 일시적으로 잠겼습니다. 잠시 후 다시 시도해주세요.",
+ "accountDeactivated": "비활성화된 계정입니다. 관리자에게 문의해주세요.",
+ "rateLimited": "로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.",
+ "systemError": "일시적인 시스템 오류입니다. 계속 문제가 발생하면 담당자에게 문의해주세요.",
+ "vendorNotFound": "등록되지 않은 벤더입니다. 담당자에게 문의해주세요.",
"newPassword": "새 비밀번호",
"newPasswordPlaceholder": "새 비밀번호를 입력하세요",
"confirmPassword": "비밀번호 확인",
diff --git a/lib/admin-users/service.ts b/lib/admin-users/service.ts
index 44111bef..b67aef20 100644
--- a/lib/admin-users/service.ts
+++ b/lib/admin-users/service.ts
@@ -5,8 +5,7 @@ import db from "@/db/db";
import logger from '@/lib/logger';
import { Role, roles, users, userView, type User, type UserView } from "@/db/schema/users"; // User 테이블
-import { type Company } from "@/db/schema/companies"; // User 테이블
-import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm";
+import { asc, desc, ilike, and, or, eq } from "drizzle-orm";
import { headers } from 'next/headers';
// 레포지토리 함수들 (예시) - 아래처럼 작성했다고 가정
@@ -99,7 +98,7 @@ export async function getUsers(input: GetUsersSchema) {
const pageCount = Math.ceil(total / input.perPage);
return { data, pageCount };
- } catch (err) {
+ } catch {
return { data: [], pageCount: 0 };
}
},
@@ -185,7 +184,7 @@ export async function findUserById(id: number) {
// }
// }
-export async function createAdminUser(input: CreateUserSchema & { language?: string }) {
+export async function createAdminUser(input: CreateUserSchema & { language?: string; phone?: string }) {
unstable_noStore(); // Next.js 캐싱 방지
try {
@@ -252,6 +251,7 @@ export async function createAdminUser(input: CreateUserSchema & { language?: str
const [newUser] = await insertUser(tx, {
name: input.name,
email: input.email,
+ phone: input.phone, // 전화번호 필드 추가
domain: input.domain,
companyId: input.companyId ?? null,
// 기타 필요한 필드 추가
@@ -300,7 +300,7 @@ export async function getUserCountGroupByCompany() {
return obj;
});
return result;
- } catch (err) {
+ } catch {
return {};
}
},
@@ -335,8 +335,7 @@ export async function getUserCountGroupByRole() {
// 여기서 result를 반환해 줘야 함!
return result;
- } catch (err) {
- console.error("getUserCountGroupByRole error:", err);
+ } catch {
return {};
}
},
@@ -349,7 +348,7 @@ export async function getUserCountGroupByRole() {
/**
* 단건 업데이트
*/
-export async function modifiUser(input: UpdateUserSchema & { id: number } & { language?: string }) {
+export async function modifiUser(input: UpdateUserSchema & { id: number } & { language?: string; phone?: string }) {
unstable_noStore();
try {
@@ -363,6 +362,7 @@ export async function modifiUser(input: UpdateUserSchema & { id: number } & { la
name: input.name,
companyId: input.companyId,
email: input.email,
+ phone: input.phone, // 전화번호 필드 추가
});
// 2) roles가 함께 왔다면, 기존 roles 삭제 → 새 roles 삽입
@@ -507,7 +507,7 @@ export async function removeUsers(input: { ids: number[] }) {
export async function getAllCompanies(): Promise<Vendor[]> {
try {
return await findAllCompanies(); // Company[]
- } catch (err) {
+ } catch {
throw new Error("Failed to get companies");
}
}
@@ -515,7 +515,7 @@ export async function getAllCompanies(): Promise<Vendor[]> {
export async function getAllRoles(): Promise<Role[]> {
try {
return await findAllRoles();
- } catch (err) {
+ } catch {
throw new Error("Failed to get roles");
}
}
diff --git a/lib/admin-users/table/add-ausers-dialog.tsx b/lib/admin-users/table/add-ausers-dialog.tsx
index 64941965..3b29adcf 100644
--- a/lib/admin-users/table/add-ausers-dialog.tsx
+++ b/lib/admin-users/table/add-ausers-dialog.tsx
@@ -47,12 +47,31 @@ import { Check, ChevronsUpDown, Loader } from "lucide-react"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
import { Vendor } from "@/db/schema/vendors"
+import { FormDescription } from "@/components/ui/form"
const languageOptions = [
{ value: "ko", label: "한국어" },
{ value: "en", label: "English" },
]
+// Phone validation helper
+const validatePhoneNumber = (phone: string): boolean => {
+ if (!phone) return true; // Optional field
+ // Basic international phone number validation
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
+ return /^\+\d{3,20}$/.test(cleanPhone);
+};
+
+// Get phone placeholder
+const getPhonePlaceholder = (): string => {
+ return "+82-10-1234-5678";
+};
+
+// Get phone description
+const getPhoneDescription = (): string => {
+ return "국제 전화번호를 입력하세요. (예: +82-10-1234-5678)";
+};
+
export function AddUserDialog() {
const [open, setOpen] = React.useState(false)
@@ -74,11 +93,12 @@ export function AddUserDialog() {
}, [])
// react-hook-form 세팅
- const form = useForm<CreateUserSchema & { language?: string }>({
+ const form = useForm<CreateUserSchema & { language?: string; phone?: string }>({
resolver: zodResolver(createUserSchema),
defaultValues: {
name: "",
email: "",
+ phone: "", // Add phone field
companyId: null,
language:'en',
// roles는 array<string>, 여기서는 단일 선택 시 [role]로 담음
@@ -89,9 +109,11 @@ export function AddUserDialog() {
})
- async function onSubmit(data: CreateUserSchema & { language?: string }) {
+ async function onSubmit(data: CreateUserSchema & { language?: string; phone?: string }) {
data.domain = "partners"
+ // Phone validation is now handled by zod schema
+
// 만약 단일 Select로 role을 정했다면, data.roles = ["manager"] 이런 식
startAddTransition(async ()=> {
const result = await createAdminUser(data)
@@ -171,6 +193,30 @@ export function AddUserDialog() {
)}
/>
+ {/* 전화번호 - 새로 추가 */}
+ <FormField
+ control={form.control}
+ name="phone"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Phone Number</FormLabel>
+ <FormControl>
+ <Input
+ placeholder={getPhonePlaceholder()}
+ {...field}
+ className={cn(
+ field.value && !validatePhoneNumber(field.value) && "border-red-500"
+ )}
+ />
+ </FormControl>
+ <FormDescription className="text-xs text-muted-foreground">
+ {getPhoneDescription()}
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
{/* 회사 선택 (companyId) */}
<FormField
control={form.control}
diff --git a/lib/admin-users/table/ausers-table.tsx b/lib/admin-users/table/ausers-table.tsx
index 1e254b5c..98319826 100644
--- a/lib/admin-users/table/ausers-table.tsx
+++ b/lib/admin-users/table/ausers-table.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { userRoles , type UserView} from "@/db/schema/users"
+import { type UserView} from "@/db/schema/users"
import type {
DataTableAdvancedFilterField,
DataTableFilterField,
@@ -12,11 +12,8 @@ import { toSentenceCase } from "@/lib/utils"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"
import type {
- getUserCountGroupByCompany,
- getUserCountGroupByRole,
getUsers, getAllCompanies,
getAllRoles
} from "@/lib//admin-users/service"
diff --git a/lib/admin-users/table/update-auser-sheet.tsx b/lib/admin-users/table/update-auser-sheet.tsx
index ddf1f932..fbd3a42f 100644
--- a/lib/admin-users/table/update-auser-sheet.tsx
+++ b/lib/admin-users/table/update-auser-sheet.tsx
@@ -23,6 +23,7 @@ import {
FormItem,
FormLabel,
FormMessage,
+ FormDescription,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
@@ -31,12 +32,12 @@ import {
SelectContent,
SelectItem,
SelectValue,
- SelectGroup,
} from "@/components/ui/select"
// import your MultiSelect or other role selection
import { MultiSelect } from "@/components/ui/multi-select"
+import { cn } from "@/lib/utils"
-import { userRoles, type UserView } from "@/db/schema/users"
+import { type UserView } from "@/db/schema/users"
import { updateUserSchema, type UpdateUserSchema } from "@/lib/admin-users/validations"
import { modifiUser } from "@/lib/admin-users/service"
@@ -50,16 +51,35 @@ const languageOptions = [
{ value: "en", label: "English" },
]
+// Phone validation helper
+const validatePhoneNumber = (phone: string): boolean => {
+ if (!phone) return true; // Optional field
+ // Basic international phone number validation
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
+ return /^\+\d{3,20}$/.test(cleanPhone);
+};
+
+// Get phone placeholder
+const getPhonePlaceholder = (): string => {
+ return "+82-10-1234-5678";
+};
+
+// Get phone description
+const getPhoneDescription = (): string => {
+ return "국제 전화번호를 입력하세요. (예: +82-10-1234-5678)";
+};
+
export function UpdateAuserSheet({ user, ...props }: UpdateAuserSheetProps) {
const [isUpdatePending, startUpdateTransition] = React.useTransition()
// 1) RHF 설정
- const form = useForm<UpdateUserSchema & { language?: string }>({
+ const form = useForm<UpdateUserSchema & { language?: string; phone?: string }>({
resolver: zodResolver(updateUserSchema),
defaultValues: {
name: user?.user_name ?? "",
email: user?.user_email ?? "",
+ phone: user?.user_phone ?? "", // Add phone field
companyId: user?.company_id ?? null,
roles: user?.roles ?? [],
language:'en',
@@ -72,15 +92,19 @@ export function UpdateAuserSheet({ user, ...props }: UpdateAuserSheetProps) {
form.reset({
name: user.user_name,
email: user.user_email,
+ phone: user.user_phone || "", // Add phone field
companyId: user.company_id,
roles: user.roles,
+ language: 'en', // You might want to get this from user object
})
}
}, [user, form])
// 3) onSubmit
- async function onSubmit(input: UpdateUserSchema & { language?: string }) {
+ async function onSubmit(input: UpdateUserSchema & { language?: string; phone?: string }) {
+ // Phone validation is now handled by zod schema
+
startUpdateTransition(async () => {
if (!user) return
@@ -147,6 +171,30 @@ export function UpdateAuserSheet({ user, ...props }: UpdateAuserSheetProps) {
)}
/>
+ {/* 전화번호 - 새로 추가 */}
+ <FormField
+ control={form.control}
+ name="phone"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Phone Number</FormLabel>
+ <FormControl>
+ <Input
+ placeholder={getPhonePlaceholder()}
+ {...field}
+ className={cn(
+ field.value && !validatePhoneNumber(field.value) && "border-red-500"
+ )}
+ />
+ </FormControl>
+ <FormDescription className="text-xs text-muted-foreground">
+ {getPhoneDescription()}
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
{/* roles */}
<FormField
control={form.control}
diff --git a/lib/admin-users/validations.ts b/lib/admin-users/validations.ts
index 3c2fdb9c..86ff8d20 100644
--- a/lib/admin-users/validations.ts
+++ b/lib/admin-users/validations.ts
@@ -9,6 +9,7 @@ import {
import * as z from "zod"
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
import { checkEmailExists } from "./service";
+import { fallbackModeToStaticPathsResult } from "next/dist/lib/fallback";
@@ -65,7 +66,21 @@ export const createUserSchema = z.object({
domain: z.enum(users.domain.enumValues), // "evcp" | "partners"
companyId: z.number().nullable().optional(), // number | null | undefined
roles:z.array(z.string()).min(1, "At least one role must be selected"),
- language: z.enum(["ko", "en"]).optional(),
+ language: z.enum(["ko", "en"]).optional(),
+ phone: z
+ .string()
+ .refine(
+ (phone) => {
+ if (!phone) return false; // Optional field
+ // Remove spaces, hyphens, and parentheses for validation
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
+ // Basic international phone number validation
+ return /^\+\d{3,20}$/.test(cleanPhone);
+ },
+ {
+ message: "올바른 국제 전화번호 형식이 아닙니다. +로 시작하는 3-20자리 번호를 입력해주세요. (예: +82-10-1234-5678)"
+ }
+ ), // 전화번호 필드 추가
});
@@ -75,7 +90,21 @@ export const updateUserSchema = z.object({
domain: z.enum(users.domain.enumValues).optional(),
companyId: z.number().nullable().optional(),
roles: z.array(z.string()).optional(),
- language: z.enum(["ko", "en"]).optional(),
+ language: z.enum(["ko", "en"]).optional(),
+ phone: z
+ .string()
+ .refine(
+ (phone) => {
+ if (!phone) return false; // Optional field
+ // Remove spaces, hyphens, and parentheses for validation
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
+ // Basic international phone number validation
+ return /^\+\d{3,20}$/.test(cleanPhone);
+ },
+ {
+ message: "올바른 국제 전화번호 형식이 아닙니다. +로 시작하는 3-20자리 번호를 입력해주세요. (예: +82-10-1234-5678)"
+ }
+ ), // 전화번호 필드 추가
});
export type GetUsersSchema = Awaited<ReturnType<typeof searchParamsCache.parse>>
diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts
index 5cb9c24f..8cb3c434 100644
--- a/lib/users/auth/verifyCredentails.ts
+++ b/lib/users/auth/verifyCredentails.ts
@@ -315,6 +315,7 @@ export async function verifyExternalCredentials(
// 타이밍 공격 방지를 위해 가짜 해시 연산
await bcrypt.compare(password, '$2a$12$fake.hash.to.prevent.timing.attacks');
await logLoginAttempt(username, null, false, 'INVALID_CREDENTIALS');
+ // 보안상 계정 존재 여부와 비밀번호 오류를 구분하지 않습니다
return { success: false, error: 'INVALID_CREDENTIALS' };
}
diff --git a/lib/users/session/helper.ts b/lib/users/session/helper.ts
index 439ab32d..f99ca80a 100644
--- a/lib/users/session/helper.ts
+++ b/lib/users/session/helper.ts
@@ -17,7 +17,7 @@ export const authHelpers = {
}
if (!authResult.success || !authResult.user) {
- return { success: false, error: 'Invalid credentials' }
+ return { success: false, error: authResult.error || 'INVALID_CREDENTIALS' }
}
// DB에 임시 인증 세션 생성
@@ -45,7 +45,7 @@ export const authHelpers = {
}
} catch (error) {
console.error('First auth error:', error)
- return { success: false, error: 'Authentication failed' }
+ return { success: false, error: 'SYSTEM_ERROR' }
}
},
diff --git a/lib/vendor-users/service.ts b/lib/vendor-users/service.ts
index 428e8b73..c6d07d3f 100644
--- a/lib/vendor-users/service.ts
+++ b/lib/vendor-users/service.ts
@@ -242,7 +242,7 @@ export async function getUserCountGroupByRoleAndVendor() {
/**
* 단건 업데이트
*/
-export async function modifiVendorUser(input: UpdateVendorUserSchema & { id: number } & { language?: string }) {
+export async function modifiVendorUser(input: UpdateVendorUserSchema & { id: number } & { language?: string; phone?: string }) {
unstable_noStore();
try {
@@ -271,6 +271,7 @@ export async function modifiVendorUser(input: UpdateVendorUserSchema & { id: num
const [res] = await updateUser(tx, input.id, {
name: input.name,
email: input.email,
+ phone: input.phone, // 전화번호 필드 추가
});
// 2) roles 업데이트 (같은 회사 내에서만)