diff options
Diffstat (limited to 'lib')
22 files changed, 804 insertions, 517 deletions
diff --git a/lib/dashboard/partners-service.ts b/lib/dashboard/partners-service.ts index 327a16a9..ac8ca920 100644 --- a/lib/dashboard/partners-service.ts +++ b/lib/dashboard/partners-service.ts @@ -73,7 +73,6 @@ export async function getPartnersTeamDashboardData(domain: string): Promise<Part } }); - console.log('๐ ํํธ๋ ํ ๋์๋ณด๋ ๊ฒฐ๊ณผ:', successfulResults); return successfulResults; } catch (error) { console.error("ํํธ๋ ํ ๋์๋ณด๋ ๋ฐ์ดํฐ ์กฐํ ์คํจ:", error); @@ -98,7 +97,6 @@ export async function getPartnersUserDashboardData(domain: string): Promise<Part return []; } - console.log(`๐ค ์ฌ์ฉ์ ID: ${userId}, ํ์ฌ ID: ${companyId}`); // ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ์ฑ๋ฅ ํฅ์ const results = await Promise.allSettled( @@ -176,7 +174,6 @@ export async function getPartnersDashboardData(domain: string): Promise<Partners // Partners ํ
์ด๋ธ๋ณ ์ ์ฒด ํต๊ณ ์กฐํ (ํ์ฌ ํํฐ๋ง ํฌํจ) async function getPartnersTableStats(config: TableConfig, companyId: string): Promise<PartnersDashboardStats> { try { - console.log(`\n๐ ํํธ๋ ํ
์ด๋ธ ${config.tableName} ํต๊ณ ์กฐํ (ํ์ฌ: ${companyId})`); // 1๋จ๊ณ: ํ์ฌ๋ณ ์ด ๊ฐ์ ํ์ธ const totalQuery = ` @@ -184,10 +181,8 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr FROM "${config.tableName}" WHERE "vendor_id" = '${companyId}' `; - console.log("Total SQL:", totalQuery); const totalResult = await db.execute(sql.raw(totalQuery)); - console.log("Total ๊ฒฐ๊ณผ:", totalResult.rows[0]); // 2๋จ๊ณ: ํ์ฌ๋ณ ์ํ๊ฐ ๋ถํฌ ํ์ธ const statusQuery = ` @@ -197,10 +192,8 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr GROUP BY "${config.statusField}" ORDER BY count DESC `; - console.log("Status SQL:", statusQuery); const statusResult = await db.execute(sql.raw(statusQuery)); - console.log("Status ๊ฒฐ๊ณผ:", statusResult.rows); // 3๋จ๊ณ: ์ํ๋ณ ๊ฐ์ ์กฐํ const pendingValues = Object.entries(config.statusMapping) @@ -215,11 +208,7 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr .filter(([_, mapped]) => mapped === 'completed') .map(([original]) => original); - console.log("ํํธ๋ ์ํ ๋งคํ:"); - console.log("- pending:", pendingValues); - console.log("- inProgress:", inProgressValues); - console.log("- completed:", completedValues); - + let pendingCount = 0; let inProgressCount = 0; let completedCount = 0; @@ -235,7 +224,6 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr const pendingResult = await db.execute(sql.raw(pendingQuery)); pendingCount = parseInt(pendingResult.rows[0]?.count || '0'); - console.log("Pending ๊ฐ์:", pendingCount); } // In Progress ๊ฐ์ (ํ์ฌ ํํฐ ํฌํจ) @@ -249,7 +237,6 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr const inProgressResult = await db.execute(sql.raw(inProgressQuery)); inProgressCount = parseInt(inProgressResult.rows[0]?.count || '0'); - console.log("InProgress ๊ฐ์:", inProgressCount); } // Completed ๊ฐ์ (ํ์ฌ ํํฐ ํฌํจ) @@ -263,7 +250,6 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr const completedResult = await db.execute(sql.raw(completedQuery)); completedCount = parseInt(completedResult.rows[0]?.count || '0'); - console.log("Completed ๊ฐ์:", completedCount); } const stats = { @@ -275,7 +261,6 @@ async function getPartnersTableStats(config: TableConfig, companyId: string): Pr completed: completedCount }; - console.log(`โ
ํํธ๋ ${config.tableName} ์ต์ข
ํต๊ณ:`, stats); return stats; } catch (error) { console.error(`โ ํํธ๋ ํ
์ด๋ธ ${config.tableName} ํต๊ณ ์กฐํ ์ค ์ค๋ฅ:`, error); @@ -288,11 +273,9 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, try { // ์ฌ์ฉ์ ํ๋๊ฐ ์๋ ๊ฒฝ์ฐ ๋น ํต๊ณ ๋ฐํ if (!hasUserFields(config)) { - console.log(`โ ๏ธ ํํธ๋ ํ
์ด๋ธ ${config.tableName}์ ์ฌ์ฉ์ ํ๋๊ฐ ์์ต๋๋ค.`); return createEmptyPartnersStats(config); } - console.log(`\n๐ค ํํธ๋ ์ฌ์ฉ์ ${userId}์ ${config.tableName} ํต๊ณ ์กฐํ (ํ์ฌ: ${companyId})`); // ์ฌ์ฉ์ ์กฐ๊ฑด ์์ฑ (ํ์ฌ ํํฐ ํฌํจ) const userConditions = []; @@ -318,10 +301,8 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, FROM "${config.tableName}" WHERE "vendor_id" = '${companyId}' AND (${userConditionStr}) `; - console.log("User Total SQL:", userTotalQuery); const userTotalResult = await db.execute(sql.raw(userTotalQuery)); - console.log("User Total ๊ฒฐ๊ณผ:", userTotalResult.rows[0]); // 2. ์ฌ์ฉ์ + ํ์ฌ ์ํ๋ณ ๊ฐ์ const pendingValues = Object.entries(config.statusMapping) @@ -351,7 +332,6 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, const userPendingResult = await db.execute(sql.raw(userPendingQuery)); userPendingCount = parseInt(userPendingResult.rows[0]?.count || '0'); - console.log("User Pending ๊ฐ์:", userPendingCount); } // User + Company In Progress ๊ฐ์ @@ -365,7 +345,6 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, const userInProgressResult = await db.execute(sql.raw(userInProgressQuery)); userInProgressCount = parseInt(userInProgressResult.rows[0]?.count || '0'); - console.log("User InProgress ๊ฐ์:", userInProgressCount); } // User + Company Completed ๊ฐ์ @@ -379,7 +358,6 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, const userCompletedResult = await db.execute(sql.raw(userCompletedQuery)); userCompletedCount = parseInt(userCompletedResult.rows[0]?.count || '0'); - console.log("User Completed ๊ฐ์:", userCompletedCount); } const stats = { @@ -391,7 +369,6 @@ async function getPartnersUserTableStats(config: TableConfig, companyId: string, completed: userCompletedCount }; - console.log(`โ
ํํธ๋ ์ฌ์ฉ์ ${config.tableName} ์ต์ข
ํต๊ณ:`, stats); return stats; } catch (error) { console.error(`โ ํํธ๋ ํ
์ด๋ธ ${config.tableName} ์ฌ์ฉ์ ํต๊ณ ์กฐํ ์ค ์ค๋ฅ:`, error); @@ -418,12 +395,10 @@ function hasUserFields(config: TableConfig): boolean { // ๋๋ฒ๊น
ํจ์: Partners ์ ์ฉ export async function simplePartnersTest(tableName: string, statusField: string, companyId: string) { try { - console.log(`\n๐งช ํํธ๋ ${tableName} ๊ฐ๋จํ ํ
์คํธ (ํ์ฌ: ${companyId}):`); // 1. ํ์ฌ๋ณ ์ด ๊ฐ์ const totalQuery = `SELECT COUNT(*) as total FROM "${tableName}" WHERE "vendor_id" = '${companyId}'`; const totalResult = await db.execute(sql.raw(totalQuery)); - console.log("ํ์ฌ๋ณ ์ด ๊ฐ์:", totalResult.rows[0]); // 2. ํ์ฌ๋ณ ์ํ ๋ถํฌ const statusQuery = ` @@ -434,7 +409,6 @@ export async function simplePartnersTest(tableName: string, statusField: string, ORDER BY count DESC `; const statusResult = await db.execute(sql.raw(statusQuery)); - console.log("ํ์ฌ๋ณ ์ํ ๋ถํฌ:", statusResult.rows); return { total: totalResult.rows[0], diff --git a/lib/dashboard/service.ts b/lib/dashboard/service.ts index 91ed5eb2..980938ad 100644 --- a/lib/dashboard/service.ts +++ b/lib/dashboard/service.ts @@ -64,7 +64,6 @@ export async function getTeamDashboardData(domain: string): Promise<DashboardSta } }); - console.log('๐ ํ ๋์๋ณด๋ ๊ฒฐ๊ณผ:', successfulResults); return successfulResults; } catch (error) { @@ -90,7 +89,6 @@ export async function getUserDashboardData(domain: string): Promise<UserDashboar return []; } - console.log(`๐ค ์ฌ์ฉ์ ID: ${userId}`); // ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ์ฑ๋ฅ ํฅ์ const results = await Promise.allSettled( @@ -259,7 +257,6 @@ async function getTableStats(config: TableConfig): Promise<DashboardStats> { completed: completedCount }; - console.log(`โ
${config.tableName} ์ต์ข
ํต๊ณ:`, stats); return stats; } catch (error) { console.error(`โ ํ
์ด๋ธ ${config.tableName} ํต๊ณ ์กฐํ ์ค ์ค๋ฅ:`, error); @@ -273,11 +270,9 @@ async function getUserTableStats(config: TableConfig, userId: string): Promise<D try { // ์ฌ์ฉ์ ํ๋๊ฐ ์๋ ๊ฒฝ์ฐ ๋น ํต๊ณ ๋ฐํ if (!hasUserFields(config)) { - console.log(`โ ๏ธ ํ
์ด๋ธ ${config.tableName}์ ์ฌ์ฉ์ ํ๋๊ฐ ์์ต๋๋ค.`); return createEmptyStats(config); } - console.log(`\n๐ค ์ฌ์ฉ์ ${userId}์ ${config.tableName} ํต๊ณ ์กฐํ`); // ์ฌ์ฉ์ ์กฐ๊ฑด ์์ฑ const userConditions = []; @@ -334,7 +329,6 @@ async function getUserTableStats(config: TableConfig, userId: string): Promise<D const userPendingResult = await db.execute(sql.raw(userPendingQuery)); userPendingCount = parseInt(userPendingResult.rows[0]?.count || '0'); - console.log("User Pending ๊ฐ์:", userPendingCount); } // User In Progress ๊ฐ์ @@ -411,16 +405,13 @@ export async function simpleTest(tableName: string, statusField: string) { ORDER BY count DESC `; const statusResult = await db.execute(sql.raw(statusQuery)); - console.log("์ํ ๋ถํฌ:", statusResult.rows); // 3. ํน์ ์ํ ํ
์คํธ const draftQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${statusField}" = 'DRAFT'`; const draftResult = await db.execute(sql.raw(draftQuery)); - console.log("DRAFT ๊ฐ์:", draftResult.rows[0]); const docConfirmedQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${statusField}" = 'Doc. Confirmed'`; const docConfirmedResult = await db.execute(sql.raw(docConfirmedQuery)); - console.log("Doc. Confirmed ๊ฐ์:", docConfirmedResult.rows[0]); return { total: totalResult.rows[0], diff --git a/lib/email-template/editor/template-content-editor.tsx b/lib/email-template/editor/template-content-editor.tsx index 4d31753c..08de53d2 100644 --- a/lib/email-template/editor/template-content-editor.tsx +++ b/lib/email-template/editor/template-content-editor.tsx @@ -167,7 +167,7 @@ export function TemplateContentEditor({ template, onUpdate }: TemplateContentEdi subject, content, sampleData, - version: template.version + 1 + version: template.version ? template.version + 1 :0 }) } else { toast.error(result.error || '์ ์ฅ์ ์คํจํ์ต๋๋ค.') @@ -188,12 +188,13 @@ export function TemplateContentEditor({ template, onUpdate }: TemplateContentEdi template.slug, sampleData, content, - // subject // ์ฃผ์ ํด์ + subject // ์ฃผ์ ํด์ ) - if (result.success) { + + if (result.success && result.data) { setPreviewHtml(result.data.html) - setPreviewSubject(result.data.subject) + setPreviewSubject(result.data.subjectHtml) if (!silent) toast.success('๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ ์์ฑ๋์์ต๋๋ค.') } else { if (!silent) toast.error(result.error || '๋ฏธ๋ฆฌ๋ณด๊ธฐ ์์ฑ์ ์คํจํ์ต๋๋ค.') diff --git a/lib/email-template/service.ts b/lib/email-template/service.ts index 13aba77b..e3ab9bed 100644 --- a/lib/email-template/service.ts +++ b/lib/email-template/service.ts @@ -789,7 +789,7 @@ export async function updateTemplateAction(slug: string, data: { content?: string; description?: string; sampleData?: Record<string, any>; - updatedBy: string; + updatedBy: number; }) { try { if (data.content) { @@ -850,14 +850,17 @@ export async function previewTemplateAction( slug: string, data: Record<string, any>, customContent?: string, - // subject: string + subject?: string ) { try { + + const html = await previewTemplate(slug, data, customContent); + const subjectHtml = await previewTemplate(slug, data, subject); return { success: true, - data: { html } + data: { html , subjectHtml} }; } catch (error) { return { diff --git a/lib/email-template/table/template-table-columns.tsx b/lib/email-template/table/template-table-columns.tsx index d20739cc..a678a20a 100644 --- a/lib/email-template/table/template-table-columns.tsx +++ b/lib/email-template/table/template-table-columns.tsx @@ -248,20 +248,20 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Templat </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem asChild> - <Link href={`/evcp/templates/${template.slug}`}> + <Link href={`/evcp/email-template/${template.slug}`}> <Eye className="mr-2 size-4" aria-hidden="true" /> - ๋ณด๊ธฐ + ์์ธ ๋ณด๊ธฐ </Link> </DropdownMenuItem> - <DropdownMenuItem + {/* <DropdownMenuItem onClick={() => { setRowAction({ type: "update", row }) }} > <Edit className="mr-2 size-4" aria-hidden="true" /> - ์์ - </DropdownMenuItem> + ์
๋ฐ์ดํธ + </DropdownMenuItem> */} <DropdownMenuItem onClick={() => { @@ -269,7 +269,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Templat }} > <Copy className="mr-2 size-4" aria-hidden="true" /> - ๋ณต์ + ๋ณต์ ํ๊ธฐ </DropdownMenuItem> <DropdownMenuSeparator /> @@ -281,7 +281,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Templat className="text-destructive focus:text-destructive" > <Trash className="mr-2 size-4" aria-hidden="true" /> - ์ญ์ + ์ญ์ ํ๊ธฐ </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> diff --git a/lib/email-template/table/update-template-sheet.tsx b/lib/email-template/table/update-template-sheet.tsx index 58da0626..d3df93f0 100644 --- a/lib/email-template/table/update-template-sheet.tsx +++ b/lib/email-template/table/update-template-sheet.tsx @@ -7,6 +7,7 @@ import { Loader } from "lucide-react" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" +import { useSession } from "next-auth/react" import { Button } from "@/components/ui/button" import { @@ -55,6 +56,13 @@ interface UpdateTemplateSheetProps export function UpdateTemplateSheet({ template, ...props }: UpdateTemplateSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() + const { data: session } = useSession(); + + // ๋๋ ๋ ์์ ํ๊ฒ + if (!session?.user?.id) { + toast.error("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค") + return + } const form = useForm<UpdateTemplateSchema>({ resolver: zodResolver(updateTemplateSchema), @@ -85,8 +93,10 @@ export function UpdateTemplateSheet({ template, ...props }: UpdateTemplateSheetP const { error } = await updateTemplateAction(template.slug, { name: input.name, description: input.description || undefined, + category: input.category === "none" ? undefined : input.category, // ์ฌ๊ธฐ์ ๋ณํ + // category๋ ์ผ๋ฐ์ ์ผ๋ก ์์ ํ์ง ์๋ ๊ฒ์ด ์ข์ง๋ง, ํ์์ ํฌํจ - updatedBy: currentUserId, + updatedBy: Number(session.user.id), }) if (error) { @@ -165,13 +175,13 @@ export function UpdateTemplateSheet({ template, ...props }: UpdateTemplateSheetP </SelectTrigger> </FormControl> <SelectContent> - <SelectItem value="">์นดํ
๊ณ ๋ฆฌ ์์</SelectItem> - {TEMPLATE_CATEGORY_OPTIONS.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> + <SelectItem value="none">์นดํ
๊ณ ๋ฆฌ ์์</SelectItem> + {TEMPLATE_CATEGORY_OPTIONS.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} +</SelectContent> </Select> <FormMessage /> </FormItem> diff --git a/lib/file-stroage.ts b/lib/file-stroage.ts index beca05ee..eab52364 100644 --- a/lib/file-stroage.ts +++ b/lib/file-stroage.ts @@ -367,7 +367,7 @@ interface SaveBufferOptions { userId?: string; } -interface SaveFileResult { +export interface SaveFileResult { success: boolean; filePath?: string; publicPath?: string; diff --git a/lib/form-list.zip b/lib/form-list.zip Binary files differdeleted file mode 100644 index 7312729e..00000000 --- a/lib/form-list.zip +++ /dev/null diff --git a/lib/sedp/get-form-tags.ts b/lib/sedp/get-form-tags.ts index d44e11f5..32f4403e 100644 --- a/lib/sedp/get-form-tags.ts +++ b/lib/sedp/get-form-tags.ts @@ -54,7 +54,7 @@ export async function importTagsFromSEDP( processedCount: number; excludedCount: number; totalEntries: number; - formCreated?: boolean; // ์๋ก ์ถ๊ฐ: form์ด ์์ฑ๋์๋์ง ์ฌ๋ถ + formCreated?: boolean; errors?: string[]; }> { try { @@ -67,29 +67,10 @@ export async function importTagsFromSEDP( // SEDP API์์ ํ๊ทธ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ const tagData = await fetchTagDataFromSEDP(projectCode, formCode); - // ๋ฐ์ดํฐ ํ์ ์ฒ๋ฆฌ - const tableName = Object.keys(tagData)[0]; - if (!tableName || !tagData[tableName]) { - throw new Error("Invalid tag data format from SEDP API"); - } - - const tagEntries: TagEntry[] = tagData[tableName]; - if (!Array.isArray(tagEntries) || tagEntries.length === 0) { - return { - processedCount: 0, - excludedCount: 0, - totalEntries: 0, - errors: ["No tag entries found in API response"] - }; - } - - // ์งํ ์ํฉ ๋ณด๊ณ - if (progressCallback) progressCallback(20); - // ํธ๋์ญ์
์ผ๋ก ๋ชจ๋ DB ์์
์ฒ๋ฆฌ return await db.transaction(async (tx) => { - // ํ๋ก์ ํธ ID ๊ฐ์ ธ์ค๊ธฐ - const projectRecord = await tx.select({ id: projects.id }) + // ํ๋ก์ ํธ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ (type ํฌํจ) + const projectRecord = await tx.select({ id: projects.id, type: projects.type }) .from(projects) .where(eq(projects.code, projectCode)) .limit(1); @@ -99,7 +80,74 @@ export async function importTagsFromSEDP( } const projectId = projectRecord[0].id; + const projectType = projectRecord[0].type; + + // ํ๋ก์ ํธ ํ์
์ ๋ฐ๋ผ packageCode๋ฅผ ์ฐพ์ ATT_ID ๊ฒฐ์ + const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; + + // packageId๋ก contractItem๊ณผ item ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ + const contractItemRecord = await tx.select({ itemId: contractItems.itemId }) + .from(contractItems) + .where(eq(contractItems.id, packageId)) + .limit(1); + + if (!contractItemRecord || contractItemRecord.length === 0) { + throw new Error(`Contract item not found for packageId: ${packageId}`); + } + + const itemRecord = await tx.select({ packageCode: items.packageCode }) + .from(items) + .where(eq(items.id, contractItemRecord[0].itemId)) + .limit(1); + + if (!itemRecord || itemRecord.length === 0) { + throw new Error(`Item not found for itemId: ${contractItemRecord[0].itemId}`); + } + + const targetPackageCode = itemRecord[0].packageCode; + + // ๋ฐ์ดํฐ ํ์ ์ฒ๋ฆฌ - tagData์ ์ฒซ ๋ฒ์งธ ํค ์ฌ์ฉ + const tableName = Object.keys(tagData)[0]; + if (!tableName || !tagData[tableName]) { + throw new Error("Invalid tag data format from SEDP API"); + } + + const allTagEntries: TagEntry[] = tagData[tableName]; + + if (!Array.isArray(allTagEntries) || allTagEntries.length === 0) { + return { + processedCount: 0, + excludedCount: 0, + totalEntries: 0, + errors: ["No tag entries found in API response"] + }; + } + + // packageCode๋ก ํํฐ๋ง - ATTRIBUTES์์ ์ง์ ๋ ATT_ID์ VALUE์ packageCode ๋น๊ต + const tagEntries = allTagEntries.filter(entry => { + if (Array.isArray(entry.ATTRIBUTES)) { + const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); + if (packageCodeAttr && packageCodeAttr.VALUE === targetPackageCode) { + return true; + } + } + return false; + }); + + if (tagEntries.length === 0) { + return { + processedCount: 0, + excludedCount: 0, + totalEntries: allTagEntries.length, + errors: [`No tag entries found with ${packageCodeAttId} attribute value matching packageCode: ${targetPackageCode}`] + }; + } + + // ์งํ ์ํฉ ๋ณด๊ณ + if (progressCallback) progressCallback(20); + + // ๋๋จธ์ง ์ฝ๋๋ ๊ธฐ์กด๊ณผ ๋์ผ... // form ID ๊ฐ์ ธ์ค๊ธฐ - ์์ผ๋ฉด ์์ฑ let formRecord = await tx.select({ id: forms.id }) .from(forms) @@ -139,7 +187,7 @@ export async function importTagsFromSEDP( // tagClass ์กฐํ (CLS_ID -> label) let tagClassLabel = firstTag.CLS_ID; // ๊ธฐ๋ณธ๊ฐ if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) // โ
์์ + const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, firstTag.CLS_ID), @@ -147,8 +195,6 @@ export async function importTagsFromSEDP( )) .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { tagClassLabel = tagClassRecord[0].label; } @@ -159,14 +205,11 @@ export async function importTagsFromSEDP( projectId, ); - // ep๊ฐ "IMEP"์ธ ๊ฒ๋ง ํํฐ๋ง - // const formMappings = allFormMappings?.filter(mapping => mapping.ep === "IMEP") || []; - // ํ์ฌ formCode์ ์ผ์นํ๋ ๋งคํ ์ฐพ๊ธฐ const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); if (targetFormMapping) { - console.log(`[IMPORT TAGS] Found IMEP form mapping for ${formCode}, creating form...`); + console.log(`[IMPORT TAGS] Found form mapping for ${formCode}, creating form...`); // form ์์ฑ const insertResult = await tx @@ -176,7 +219,7 @@ export async function importTagsFromSEDP( formCode: targetFormMapping.formCode, formName: targetFormMapping.formName, eng: true, // ENG ๋ชจ๋์์ ๊ฐ์ ธ์ค๋ ๊ฒ์ด๋ฏ๋ก eng: true - im: targetFormMapping.ep === "IMEP" ? true:false + im: targetFormMapping.ep === "IMEP" ? true : false }) .returning({ id: forms.id }); @@ -185,9 +228,9 @@ export async function importTagsFromSEDP( console.log(`[IMPORT TAGS] Successfully created form:`, insertResult[0]); } else { - console.log(`[IMPORT TAGS] No IMEP form mapping found for formCode: ${formCode}`); - console.log(`[IMPORT TAGS] Available IMEP mappings:`, formMappings.map(m => m.formCode)); - throw new Error(`Form ${formCode} not found and no IMEP mapping available for tag type ${tagTypeDescription}`); + console.log(`[IMPORT TAGS] No form mapping found for formCode: ${formCode}`); + console.log(`[IMPORT TAGS] Available mappings:`, allFormMappings.map(m => m.formCode)); + throw new Error(`Form ${formCode} not found and no mapping available for tag type ${tagTypeDescription}`); } } else { throw new Error(`Form not found for formCode: ${formCode} and contractItemId: ${packageId}, and no tags to derive form mapping`); @@ -232,7 +275,7 @@ export async function importTagsFromSEDP( // tagClass ์กฐํ let tagClassLabel = firstTag.CLS_ID; if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) // โ
์์ + const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, firstTag.CLS_ID), @@ -250,9 +293,6 @@ export async function importTagsFromSEDP( projectId, ); - // ep๊ฐ "IMEP"์ธ ๊ฒ๋ง ํํฐ๋ง - // const formMappings = allFormMappings?.filter(mapping => mapping.ep === "IMEP") || []; - // ํ์ฌ formCode์ ์ผ์นํ๋ ๋งคํ ์ฐพ๊ธฐ const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); @@ -292,6 +332,9 @@ export async function importTagsFromSEDP( const formId = formRecord[0].id; + // ๋๋จธ์ง ์ฒ๋ฆฌ ๋ก์ง์ ๊ธฐ์กด๊ณผ ๋์ผ... + // (์์ ๋ฉํ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ, ํ๊ทธ ์ฒ๋ฆฌ ๋ฑ) + // ์์ ๋ฉํ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ const formMetaRecord = await tx.select({ columns: formMetas.columns }) .from(formMetas) @@ -388,7 +431,7 @@ export async function importTagsFromSEDP( let tagClassLabel = tagEntry.CLS_ID; // ๊ธฐ๋ณธ๊ฐ let tagClassId = null; // ๊ธฐ๋ณธ๊ฐ if (tagEntry.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) // โ
์์ + const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, tagEntry.CLS_ID), @@ -414,7 +457,7 @@ export async function importTagsFromSEDP( contractItemId: packageId, formId: formId, tagNo: tagEntry.TAG_NO, - tagType: tagTypeDescription, + tagType: tagTypeDescription || "", class: tagClassLabel, tagClassId: tagClassId, description: tagEntry.TAG_DESC || null, @@ -644,7 +687,7 @@ export async function importTagsFromSEDP( processedCount, excludedCount, totalEntries: tagEntries.length, - formCreated, // ์๋ก ์ถ๊ฐ: form์ด ์์ฑ๋์๋์ง ์ฌ๋ถ + formCreated, errors: errors.length > 0 ? errors : undefined }; }); @@ -654,7 +697,6 @@ export async function importTagsFromSEDP( throw error; } } - /** * SEDP API์์ ํ๊ทธ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ * diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts index c3c88f07..f9e63caf 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -430,23 +430,7 @@ async function getRegisters(projectCode: string): Promise<Register[]> { } // ๊ฒฐ๊ณผ๋ฅผ ๋ฐฐ์ด๋ก ๋ณํ (๋จ์ผ ๊ฐ์ฒด์ธ ๊ฒฝ์ฐ ๋ฐฐ์ด๋ก ๋ํ) - let registers: Register[] = Array.isArray(data) ? data : [data]; - - // MAP_CLS_ID๊ฐ ๋น์ด์์ง ์๊ณ REMARK๊ฐ vd, VD, vD, Vd ์ค ํ๋์ธ ๋ ์ง์คํฐ๋ง ํํฐ๋ง - registers = registers.filter(register => { - // ์ญ์ ๋ ๋ ์ง์คํฐ ์ ์ธ - if (register.DELETED) return false; - - // MAP_CLS_ID ๋ฐฐ์ด์ด ์กด์ฌํ๊ณ ์์๊ฐ ํ๋ ์ด์ ์๋์ง ํ์ธ - const hasValidMapClsId = Array.isArray(register.MAP_CLS_ID) && register.MAP_CLS_ID.length > 0; - - // REMARK๊ฐ 'vd_' ๋๋ 'vd' ํฌํจ ํ์ธ (๋์๋ฌธ์ ๊ตฌ๋ถ ์์ด) - const remarkLower = register.REMARK && register.REMARK.toLowerCase(); - const hasValidRemark = remarkLower && (remarkLower.includes('vd')); - - // ๋ ์กฐ๊ฑด ๋ชจ๋ ์ถฉ์กฑํด์ผ ํจ - return hasValidMapClsId && hasValidRemark; - }); + const registers: Register[] = Array.isArray(data) ? data : [data]; console.log(`ํ๋ก์ ํธ ${projectCode}์์ ${registers.length}๊ฐ์ ์ ํจํ ๋ ์ง์คํฐ๋ฅผ ๊ฐ์ ธ์์ต๋๋ค.`); return registers; diff --git a/lib/soap/mdg/send/vendor-master/action.ts b/lib/soap/mdg/send/vendor-master/action.ts index 24112316..ec267bbb 100644 --- a/lib/soap/mdg/send/vendor-master/action.ts +++ b/lib/soap/mdg/send/vendor-master/action.ts @@ -18,6 +18,7 @@ import { } from "@/db/schema/MDG/mdg"; import { eq, sql, desc } from "drizzle-orm"; import { withSoapLogging } from "../../utils"; +// public ๊ฒฝ๋ก ์ฌ์ฉํ์ง ์์. ํ์ ๊ฒฝ๋ก ์ฌ์ฉ. (์ฒ๋ฆฌ์๋ฃ) import fs from 'fs'; import path from 'path'; import { XMLBuilder } from 'fast-xml-parser'; @@ -49,7 +50,7 @@ function parseCsv(content: string): CsvField[] { } // ๋ชจ๋ ์ด๊ธฐํ ์ CSV ๋ก๋ -const CSV_PATH = path.join(process.cwd(), 'public', 'wsdl', 'P2MD3007_AO.csv'); +const CSV_PATH = path.join(__dirname, 'P2MD3007_AO.csv'); let CSV_FIELDS: CsvField[] = []; try { const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8'); @@ -699,5 +700,3 @@ export async function getVendorSendStatistics(): Promise<{ throw error; } } - - diff --git a/lib/tech-vendors/repository.ts b/lib/tech-vendors/repository.ts index a273bf50..6f9aafbf 100644 --- a/lib/tech-vendors/repository.ts +++ b/lib/tech-vendors/repository.ts @@ -349,7 +349,6 @@ export async function getVendorWorkTypes( })
.from(techVendorPossibleItems)
.where(eq(techVendorPossibleItems.vendorId, vendorId));
- console.log("possibleItems", possibleItems);
if (!possibleItems.length) {
return [];
}
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index 41572a93..acf67497 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -730,14 +730,14 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps onSuccess={handleRefreshData}
/>
- {/* ๋ค์ค ๋ฒค๋ ์ญ์ ํ์ธ ๋ค์ด์ผ๋ก๊ทธ */}
+ {/* ๋ค์ค ๋ฒค๋ ์ญ์ ํ์ธ ๋ค์ด์ผ๋ก๊ทธ
<DeleteVendorDialog
open={deleteConfirmDialogOpen}
onOpenChange={setDeleteConfirmDialogOpen}
vendors={selectedRows}
onConfirm={executeDeleteVendors}
isLoading={isDeletingVendors}
- />
+ /> */}
{/* ๊ฒฌ์ ํ์คํ ๋ฆฌ ๋ค์ด์ผ๋ก๊ทธ */}
<QuotationHistoryDialog
diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts index 83a2f276..a5dbab41 100644 --- a/lib/users/auth/verifyCredentails.ts +++ b/lib/users/auth/verifyCredentails.ts @@ -3,6 +3,7 @@ import bcrypt from 'bcryptjs'; import crypto from 'crypto'; +// (์ฒ๋ฆฌ ๋ถํ์) ํค ์ํธํ๋ฅผ ์ํ fs ๋ชจ๋ ์ฌ์ฉ, ํ์ ๊ฒฝ๋ก ์ฌ์ฉํ๋ฉฐ public ๊ฒฝ๋ก ์๋๋ฏ๋ก ํ์ผ์ด ๋
ธ์ถ๋์ง ์์. import fs from 'fs'; import path from 'path'; import { eq, and, desc, gte, count } from 'drizzle-orm'; diff --git a/lib/users/service.ts b/lib/users/service.ts index 7a635113..80c346fa 100644 --- a/lib/users/service.ts +++ b/lib/users/service.ts @@ -5,7 +5,6 @@ import { Otp } from '@/types/user'; import { getAllUsers, createUser, getUserById, updateUser, deleteUser, getUserByEmail, createOtp,getOtpByEmailAndToken, updateOtp, findOtpByEmail ,getOtpByEmailAndCode, findAllRoles, getRoleAssignedUsers} from './repository'; import logger from '@/lib/logger'; import { Role, roles, userRoles, users, userView, type User } from '@/db/schema/users'; -import { saveDocument } from '../storage'; import { GetSimpleUsersSchema, GetUsersSchema } from '../admin-users/validations'; import { revalidatePath, revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; import { filterColumns } from '../filter-columns'; @@ -15,6 +14,7 @@ import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, ne } from "drizzle-orm"; +import { SaveFileResult, saveFile } from '../file-stroage'; interface AssignUsersArgs { roleId: number @@ -232,41 +232,159 @@ export async function findEmailTemp(email: string) { } } -export async function updateUserProfileImage(formData: FormData) { +/** + * ํ๋กํ ์
๋ฐ์ดํธ ๊ฒฐ๊ณผ ์ธํฐํ์ด์ค + */ +interface ProfileUpdateResult { + success: boolean; + user?: any; + error?: string; + duration?: number; + fileInfo?: { + fileName: string; + originalName: string; + fileSize: string; + publicPath: string; + }; + securityChecks?: { + extensionCheck: boolean; + fileNameCheck: boolean; + sizeCheck: boolean; + mimeTypeCheck: boolean; + contentCheck: boolean; + }; +} + +/** + * ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ (ํ๋กํ ์
๋ฐ์ดํธ์ฉ) + */ +const validateUserData = { + validateEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }, + + validateName(name: string): boolean { + return name.length >= 2 && + name.length <= 50 && + !/[<>:"'|?*]/.test(name); + }, +}; + +/** + * ํ์ผ ํฌ๊ธฐ๋ฅผ ์ฝ๊ธฐ ์ฌ์ด ํํ๋ก ๋ณํ + */ +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +/** + * ๋ณด์ ๊ฐํ๋ ์ฌ์ฉ์ ํ๋กํ ์ด๋ฏธ์ง ์
๋ฐ์ดํธ ํจ์ + * (file-storage.ts์ saveFile ํจ์ ํ์ฉ) + */ +export async function updateUserProfileImage(formData: FormData): Promise<ProfileUpdateResult> { + const startTime = Date.now(); + // 1) FormData์์ ๋ฐ์ดํฐ ๊บผ๋ด๊ธฐ - const file = formData.get("file") as File | null - const userId = Number(formData.get("userId")) - const name = formData.get("name") as string - const email = formData.get("email") as string - - // 2) ๊ธฐ๋ณธ์ ์ธ ์ ํจ์ฑ ๊ฒ์ฆ - if (!file) { - throw new Error("No file found in the FormData.") - } - if (!userId) { - throw new Error("userId is required.") - } + const file = formData.get("file") as File | null; + const userId = Number(formData.get("userId")); + const name = formData.get("name") as string; + const email = formData.get("email") as string; try { - // 3) ํ์ผ ์ ์ฅ (ํด์ ์์ฑ) - const directory = './public/profiles' - const { hashedFileName } = await saveDocument(file, directory) - - // 4) DB ์
๋ฐ์ดํธ - const imageUrl = hashedFileName - const data = { name, email, imageUrl } - const user = await updateUser(userId, data) + // 2) ๊ธฐ๋ณธ ์ ํจ์ฑ ๊ฒ์ฆ + if (!file) { + throw new Error("ํ์ผ์ด ์ ๊ณต๋์ง ์์์ต๋๋ค."); + } + + if (!userId || userId <= 0) { + throw new Error("์ ํจํ ์ฌ์ฉ์ ID๊ฐ ํ์ํฉ๋๋ค."); + } + + // 3) ์ฌ์ฉ์ ์ ๋ณด ๊ฒ์ฆ (ํ์ผ ์ ์ฅ ์ ์ ๋ฏธ๋ฆฌ ์ฒดํฌ) + if (name && !validateUserData.validateName(name)) { + throw new Error("์ ํจํ์ง ์์ ์ด๋ฆ์
๋๋ค (2-50์, ํน์๋ฌธ์ ์ ํ)."); + } + + if (email && !validateUserData.validateEmail(email)) { + throw new Error("์ ํจํ์ง ์์ ์ด๋ฉ์ผ ์ฃผ์์
๋๋ค."); + } + + console.log("๐ค ํ๋กํ ์ด๋ฏธ์ง ์
๋ฐ์ดํธ ์์:", { + fileName: file.name, + fileSize: formatFileSize(file.size), + userId, + }); + + // 4) file-storage.ts์ saveFile ํจ์๋ก ๋ณด์ ๊ฒ์ฆ + ์ ์ฅ (ํ ๋ฒ์ ์ฒ๋ฆฌ!) + const saveResult: SaveFileResult = await saveFile({ + file, + directory: 'profiles', // './public/profiles'์์ 'profiles'๋ก ๋ณ๊ฒฝ + originalName: file.name, + userId: userId.toString(), + }); + + // 5) ํ์ผ ์ ์ฅ ์คํจ ์ ์๋ฌ ์ฒ๋ฆฌ + if (!saveResult.success) { + throw new Error(saveResult.error || "ํ์ผ ์ ์ฅ์ ์คํจํ์ต๋๋ค."); + } + + console.log("โ
ํ์ผ ์ ์ฅ ์ฑ๊ณต:", { + fileName: saveResult.fileName, + publicPath: saveResult.publicPath, + securityChecks: saveResult.securityChecks, + }); + + // 6) DB ์
๋ฐ์ดํธ (file-storage.ts์์ ๋ฐํ๋ publicPath ์ฌ์ฉ) + const imageUrl = saveResult.publicPath; // ์น์์ ์ ๊ทผ ๊ฐ๋ฅํ ๊ฒฝ๋ก + const data = { name, email, imageUrl }; + const user = await updateUser(userId, data); + if (!user) { - // updateUser๊ฐ null์ ๋ฐํํ๋ฉด, DB ์
๋ฐ์ดํธ ์คํจ ํน์ ํด๋น ์ ์ ๊ฐ ์์ - throw new Error(`User with id=${userId} not found or update failed.`) + // DB ์
๋ฐ์ดํธ ์คํจ ์ ์
๋ก๋๋ ํ์ผ ์ ๋ฆฌ + // TODO: file-storage.ts์ deleteFile ํจ์ ์ฌ์ฉํ์ฌ ํ์ผ ์ญ์ ๊ณ ๋ ค + throw new Error(`์ฌ์ฉ์ ID=${userId}๋ฅผ ์ฐพ์ ์ ์๊ฑฐ๋ ์
๋ฐ์ดํธ์ ์คํจํ์ต๋๋ค.`); } - // 5) ์ฑ๊ณต ์ ์ฑ๊ณต ์ ๋ณด ๋ฐํ - return { success: true, user } + // 7) ์ฑ๊ณต ๋ฐํ + const duration = Date.now() - startTime; + + console.log("๐ ํ๋กํ ์
๋ฐ์ดํธ ์ฑ๊ณต:", { + userId, + imageUrl, + duration: `${duration}ms`, + }); + + return { + success: true, + user, + duration, + fileInfo: { + fileName: saveResult.fileName!, + originalName: saveResult.originalName!, + fileSize: formatFileSize(saveResult.fileSize!), + publicPath: saveResult.publicPath!, + }, + securityChecks: saveResult.securityChecks, + }; + } catch (err: any) { - // DB ์
๋ฐ์ดํธ ์ค ๋ฐ์ํ๋ ์๋ฌ๋ saveDocument ๋ด๋ถ ์๋ฌ ๋ฑ์ ์ฒ๋ฆฌ - console.error("[updateUserProfileImage] Error:", err) - throw new Error(err.message ?? "Failed to update user profile.") + const duration = Date.now() - startTime; + const errorMessage = err.message ?? "ํ๋กํ ์
๋ฐ์ดํธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."; + + console.error("โ [updateUserProfileImage] Error:", err); + + return { + success: false, + error: errorMessage, + duration, + }; } } diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index d98a4c70..032b028c 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -17,6 +17,19 @@ export interface DOLCEUploadResult { } } +interface ResultData { + FileId: string; + UploadId: string; + FileSeq: number; + FileName: string; + FileRelativePath: string; + FileSize: number; + FileCreateDT: string; // ISO string format + FileWriteDT: string; // ISO string format + OwnerUserId: string; +} + + interface FileReaderConfig { baseDir: string; isProduction: boolean; @@ -339,8 +352,10 @@ private async uploadFiles( uploadId: string // ์ด๋ฏธ ์์ฑ๋ UploadId๋ฅผ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ ): Promise<Array<{ uploadId: string, fileId: string, filePath: string }>> { const uploadResults = [] + const resultDataArray: ResultData[] = [] - for (const attachment of attachments) { + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i] try { // FileId๋ง ์๋ก ์์ฑ (UploadId๋ ์ด๋ฏธ ์์ฑ๋ ๊ฒ ์ฌ์ฉ) const fileId = uuidv4() @@ -386,6 +401,23 @@ private async uploadFiles( filePath: dolceFilePath }) + // ResultData ๊ฐ์ฒด ์์ฑ (PWPUploadResultService ํธ์ถ์ฉ) + const fileStats = await this.getFileStats(attachment.filePath) // ํ์ผ ํต๊ณ ์ ๋ณด ์กฐํ + + const resultData: ResultData = { + FileId: fileId, + UploadId: uploadId, + FileSeq: i + 1, // 1๋ถํฐ ์์ํ๋ ์ํ์ค + FileName: attachment.fileName, + FileRelativePath: dolceFilePath, + FileSize: fileStats.size, + FileCreateDT: fileStats.birthtime.toISOString(), + FileWriteDT: fileStats.mtime.toISOString(), + OwnerUserId: userId + } + + resultDataArray.push(resultData) + console.log(`โ
File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`) console.log(`โ
DB updated for attachment ID: ${attachment.id}`) @@ -395,9 +427,82 @@ private async uploadFiles( } } + // ๋ชจ๋ ํ์ผ ์
๋ก๋๊ฐ ์๋ฃ๋ ํ PWPUploadResultService ํธ์ถ + if (resultDataArray.length > 0) { + try { + await this.finalizeUploadResult(resultDataArray) + console.log(`โ
Upload result finalized for UploadId: ${uploadId}`) + } catch (error) { + console.error(`โ Failed to finalize upload result for UploadId: ${uploadId}`, error) + // ํ์ผ ์
๋ก๋๋ ์ฑ๊ณตํ์ง๋ง ๊ฒฐ๊ณผ ์ ์ฅ ์คํจ - ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ๊ณ์ ์งํ + } + } + return uploadResults } + +private async finalizeUploadResult(resultDataArray: ResultData[]): Promise<void> { + const url = `${this.UPLOAD_SERVICE_URL}/PWPUploadResultService.ashx` + + try { + const jsonData = JSON.stringify(resultDataArray) + const dataBuffer = Buffer.from(jsonData, 'utf-8') + + console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`) + console.log('ResultData:', JSON.stringify(resultDataArray, null, 2)) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: dataBuffer + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.text() + + if (result !== 'Success') { + throw new Error(`PWPUploadResultService returned unexpected result: ${result}`) + } + + console.log('โ
PWPUploadResultService call successful') + + } catch (error) { + console.error('โ PWPUploadResultService call failed:', error) + throw error + } +} + +// ํ์ผ ํต๊ณ ์ ๋ณด ์กฐํ ํฌํผ ๋ฉ์๋ (ํ์ผ์์คํ
์์ ํ์ผ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ด) +private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> { + try { + // Node.js ํ๊ฒฝ์ด๋ผ๋ฉด fs.stat ์ฌ์ฉ + const fs = require('fs').promises + const stats = await fs.stat(filePath) + + return { + size: stats.size, + birthtime: stats.birthtime, + mtime: stats.mtime + } + } catch (error) { + console.warn(`Could not get file stats for ${filePath}, using defaults`) + // ํ์ผ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ + const now = new Date() + return { + size: 0, + birthtime: now, + mtime: now + } + } +} + /** * ๋ฌธ์ ์ ๋ณด ์
๋ก๋ (DetailDwgReceiptMgmtEdit) */ diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index e01283dc..6fe8feb7 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -20,7 +20,7 @@ import type { Revision } from "@/types/enhanced-documents" import { GetVendorShipDcoumentsSchema } from "./validations" -import { contracts, users } from "@/db/schema" +import { contracts, users, vendors } from "@/db/schema" // ์คํค๋ง ํ์
์ ์ export interface GetEnhancedDocumentsSchema { @@ -1092,11 +1092,12 @@ export async function getDocumentDetails(documentId: number) { // ๋ฒค๋ ์ ๋ณด ์กฐํ const [vendorInfo] = await tx .select({ - vendorName: simplifiedDocumentsView.vendorName, - vendorCode: simplifiedDocumentsView.vendorCode, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, }) - .from(simplifiedDocumentsView) - .where(eq(simplifiedDocumentsView.contractId, contractIds[0])) + .from(contracts) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractIds[0])) .limit(1) return { data, total, drawingKind, vendorInfo } diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index bc384ea2..c7ba041a 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -1332,156 +1332,189 @@ class ImportService { /** * ๊ฐ์ ธ์ค๊ธฐ ์ํ ์กฐํ */ - async getImportStatus( - contractId: number, - sourceSystem: string = 'DOLCE' - ): Promise<ImportStatus> { - try { - // ๋ง์ง๋ง ๊ฐ์ ธ์ค๊ธฐ ์๊ฐ ์กฐํ - const [lastImport] = await db - .select({ - lastSynced: sql<string>`MAX(${documents.externalSyncedAt})` - }) - .from(documents) - .where(and( - eq(documents.contractId, contractId), - eq(documents.externalSystemType, sourceSystem) - )) + /** + * ๊ฐ์ ธ์ค๊ธฐ ์ํ ์กฐํ - ์๋ฌ ์ ์์ ํ ๊ธฐ๋ณธ๊ฐ ๋ฐํ + */ +async getImportStatus( + contractId: number, + sourceSystem: string = 'DOLCE' +): Promise<ImportStatus> { + try { + // ๋ง์ง๋ง ๊ฐ์ ธ์ค๊ธฐ ์๊ฐ ์กฐํ + const [lastImport] = await db + .select({ + lastSynced: sql<string>`MAX(${documents.externalSyncedAt})` + }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.externalSystemType, sourceSystem) + )) - // ํ๋ก์ ํธ ์ฝ๋์ ๋ฒค๋ ์ฝ๋ ์กฐํ - const contractInfo = await this.getContractInfoById(contractId) + // ํ๋ก์ ํธ ์ฝ๋์ ๋ฒค๋ ์ฝ๋ ์กฐํ + const contractInfo = await this.getContractInfoById(contractId) - if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { - throw new Error(`Project code or vendor code not found for contract ${contractId}`) + // ๐ฅ ๊ณ์ฝ ์ ๋ณด๊ฐ ์์ผ๋ฉด ๊ธฐ๋ณธ ์ํ ๋ฐํ (์๋ฌ throw ํ์ง ์์) + if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { + console.warn(`Project code or vendor code not found for contract ${contractId}`) + return { + lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + availableDocuments: 0, + newDocuments: 0, + updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, + importEnabled: false, // ๐ฅ ๊ณ์ฝ ์ ๋ณด๊ฐ ์์ผ๋ฉด import ๋นํ์ฑํ + error: `Contract ${contractId}์ ๋ํ ํ๋ก์ ํธ ์ฝ๋ ๋๋ ๋ฒค๋ ์ฝ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.` // ๐ฅ ์๋ฌ ๋ฉ์์ง ์ถ๊ฐ } + } - let availableDocuments = 0 - let newDocuments = 0 - let updatedDocuments = 0 - let availableRevisions = 0 - let newRevisions = 0 - let updatedRevisions = 0 - let availableAttachments = 0 - let newAttachments = 0 - let updatedAttachments = 0 + let availableDocuments = 0 + let newDocuments = 0 + let updatedDocuments = 0 + let availableRevisions = 0 + let newRevisions = 0 + let updatedRevisions = 0 + let availableAttachments = 0 + let newAttachments = 0 + let updatedAttachments = 0 - try { - // ๊ฐ drawingKind๋ณ๋ก ํ์ธ - const drawingKinds = ['B3', 'B4', 'B5'] + try { + // ๊ฐ drawingKind๋ณ๋ก ํ์ธ + const drawingKinds = ['B3', 'B4', 'B5'] - for (const drawingKind of drawingKinds) { - try { - const externalDocs = await this.fetchFromDOLCE( - contractInfo.projectCode, - contractInfo.vendorCode, - drawingKind - ) - availableDocuments += externalDocs.length - - // ์ ๊ท/์
๋ฐ์ดํธ ๋ฌธ์ ์ ๊ณ์ฐ - for (const externalDoc of externalDocs) { - const existing = await db - .select({ id: documents.id, updatedAt: documents.updatedAt }) - .from(documents) - .where(and( - eq(documents.contractId, contractId), - eq(documents.docNumber, externalDoc.DrawingNo) - )) - .limit(1) - - if (existing.length === 0) { - newDocuments++ - } else { - // DOLCE์ CreateDt์ ๋ก์ปฌ updatedAt ๋น๊ต - if (externalDoc.CreateDt && existing[0].updatedAt) { - const externalModified = new Date(externalDoc.CreateDt) - const localModified = new Date(existing[0].updatedAt) - if (externalModified > localModified) { - updatedDocuments++ - } + for (const drawingKind of drawingKinds) { + try { + const externalDocs = await this.fetchFromDOLCE( + contractInfo.projectCode, + contractInfo.vendorCode, + drawingKind + ) + availableDocuments += externalDocs.length + + // ์ ๊ท/์
๋ฐ์ดํธ ๋ฌธ์ ์ ๊ณ์ฐ + for (const externalDoc of externalDocs) { + const existing = await db + .select({ id: documents.id, updatedAt: documents.updatedAt }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.docNumber, externalDoc.DrawingNo) + )) + .limit(1) + + if (existing.length === 0) { + newDocuments++ + } else { + // DOLCE์ CreateDt์ ๋ก์ปฌ updatedAt ๋น๊ต + if (externalDoc.CreateDt && existing[0].updatedAt) { + const externalModified = new Date(externalDoc.CreateDt) + const localModified = new Date(existing[0].updatedAt) + if (externalModified > localModified) { + updatedDocuments++ } } + } - // revisions ๋ฐ attachments ์ํ๋ ํ์ธ - try { - const detailDocs = await this.fetchDetailFromDOLCE( - externalDoc.ProjectNo, - externalDoc.DrawingNo, - externalDoc.Discipline, - externalDoc.DrawingKind - ) - availableRevisions += detailDocs.length - - for (const detailDoc of detailDocs) { - const existingRevision = await db - .select({ id: revisions.id }) - .from(revisions) - .where(eq(revisions.registerId, detailDoc.RegisterId)) - .limit(1) - - if (existingRevision.length === 0) { - newRevisions++ - } else { - updatedRevisions++ - } + // revisions ๋ฐ attachments ์ํ๋ ํ์ธ + try { + const detailDocs = await this.fetchDetailFromDOLCE( + externalDoc.ProjectNo, + externalDoc.DrawingNo, + externalDoc.Discipline, + externalDoc.DrawingKind + ) + availableRevisions += detailDocs.length + + for (const detailDoc of detailDocs) { + const existingRevision = await db + .select({ id: revisions.id }) + .from(revisions) + .where(eq(revisions.registerId, detailDoc.RegisterId)) + .limit(1) + + if (existingRevision.length === 0) { + newRevisions++ + } else { + updatedRevisions++ + } - // FS Category ๋ฌธ์์ ์ฒจ๋ถํ์ผ ํ์ธ - if (detailDoc.Category === 'FS' && detailDoc.UploadId) { - try { - const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) - availableAttachments += fileInfos.filter(f => f.UseYn === 'Y').length - - for (const fileInfo of fileInfos) { - if (fileInfo.UseYn !== 'Y') continue - - const existingAttachment = await db - .select({ id: documentAttachments.id }) - .from(documentAttachments) - .where(eq(documentAttachments.fileId, fileInfo.FileId)) - .limit(1) - - if (existingAttachment.length === 0) { - newAttachments++ - } else { - updatedAttachments++ - } + // FS Category ๋ฌธ์์ ์ฒจ๋ถํ์ผ ํ์ธ + if (detailDoc.Category === 'FS' && detailDoc.UploadId) { + try { + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + availableAttachments += fileInfos.filter(f => f.UseYn === 'Y').length + + for (const fileInfo of fileInfos) { + if (fileInfo.UseYn !== 'Y') continue + + const existingAttachment = await db + .select({ id: documentAttachments.id }) + .from(documentAttachments) + .where(eq(documentAttachments.fileId, fileInfo.FileId)) + .limit(1) + + if (existingAttachment.length === 0) { + newAttachments++ + } else { + updatedAttachments++ } - } catch (error) { - console.warn(`Failed to check files for ${detailDoc.UploadId}:`, error) } + } catch (error) { + console.warn(`Failed to check files for ${detailDoc.UploadId}:`, error) } } - } catch (error) { - console.warn(`Failed to check revisions for ${externalDoc.DrawingNo}:`, error) } + } catch (error) { + console.warn(`Failed to check revisions for ${externalDoc.DrawingNo}:`, error) } - } catch (error) { - console.warn(`Failed to check ${drawingKind} for status:`, error) } + } catch (error) { + console.warn(`Failed to check ${drawingKind} for status:`, error) } - } catch (error) { - console.warn(`Failed to fetch external data for status: ${error}`) } + } catch (error) { + console.warn(`Failed to fetch external data for status: ${error}`) + // ๐ฅ ์ธ๋ถ API ํธ์ถ ์คํจ ์์๋ ๊ธฐ๋ณธ๊ฐ ๋ฐํ + } - return { - lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, - availableDocuments, - newDocuments, - updatedDocuments, - availableRevisions, - newRevisions, - updatedRevisions, - availableAttachments, - newAttachments, - updatedAttachments, - importEnabled: this.isImportEnabled(sourceSystem) - } + return { + lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + availableDocuments, + newDocuments, + updatedDocuments, + availableRevisions, + newRevisions, + updatedRevisions, + availableAttachments, + newAttachments, + updatedAttachments, + importEnabled: this.isImportEnabled(sourceSystem) + } - } catch (error) { - console.error('Failed to get import status:', error) - throw error + } catch (error) { + // ๐ฅ ์ต์ข
์ ์ผ๋ก ๋ชจ๋ ์๋ฌ๋ฅผ catchํ์ฌ ์์ ํ ๊ธฐ๋ณธ๊ฐ ๋ฐํ + console.error('Failed to get import status:', error) + return { + lastImportAt: undefined, + availableDocuments: 0, + newDocuments: 0, + updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, + importEnabled: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' } } +} /** * ๊ฐ์ ธ์ค๊ธฐ ํ์ฑํ ์ฌ๋ถ ํ์ธ diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx index 9c13573c..51c104dc 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx @@ -59,7 +59,7 @@ export function getSimplifiedDocumentColumns({ id: "select", header: ({ table }) => ( <div className="flex items-center justify-center"> - <span className="text-xs text-gray-500">์ ํ</span> + <span className="text-xs text-gray-500">Select</span> </div> ), cell: ({ row }) => { @@ -78,7 +78,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "docNumber", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="๋ฌธ์๋ฒํธ" /> + <DataTableColumnHeaderSimple column={column} title="Document No" /> ), cell: ({ row }) => { const doc = row.original @@ -90,7 +90,7 @@ export function getSimplifiedDocumentColumns({ size: 120, enableResizing: true, meta: { - excelHeader: "๋ฌธ์๋ฒํธ" + excelHeader: "Document No" }, }, @@ -98,7 +98,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "title", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="๋ฌธ์๋ช
" /> + <DataTableColumnHeaderSimple column={column} title="Title" /> ), cell: ({ row }) => { const doc = row.original @@ -110,7 +110,7 @@ export function getSimplifiedDocumentColumns({ enableResizing: true, maxSize:300, meta: { - excelHeader: "๋ฌธ์๋ช
" + excelHeader: "Title" }, }, @@ -118,7 +118,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "projectCode", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="ํ๋ก์ ํธ" /> + <DataTableColumnHeaderSimple column={column} title="Project" /> ), cell: ({ row }) => { const projectCode = row.original.projectCode @@ -131,7 +131,7 @@ export function getSimplifiedDocumentColumns({ maxSize:100, meta: { - excelHeader: "ํ๋ก์ ํธ" + excelHeader: "Project" }, }, @@ -141,7 +141,7 @@ export function getSimplifiedDocumentColumns({ header: ({ table }) => { // ์ฒซ ๋ฒ์งธ ํ์ firstStageName์ ๊ทธ๋ฃน ํค๋๋ก ์ฌ์ฉ const firstRow = table.getRowModel().rows[0]?.original - const stageName = firstRow?.firstStageName || "1์ฐจ ์คํ
์ด์ง" + const stageName = firstRow?.firstStageName || "First Stage" return ( <div className="text-center font-medium text-gray-700"> {stageName} @@ -152,27 +152,27 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "firstStagePlanDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="๊ณํ์ผ" /> + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> ), cell: ({ row }) => { return <FirstStagePlanDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "1์ฐจ ๊ณํ์ผ" + excelHeader: "First Planned Date" }, }, { accessorKey: "firstStageActualDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="์ค์ ์ผ" /> + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> ), cell: ({ row }) => { return <FirstStageActualDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "1์ฐจ ์ค์ ์ผ" + excelHeader: "First Actual Date" }, }, ], @@ -184,7 +184,7 @@ export function getSimplifiedDocumentColumns({ header: ({ table }) => { // ์ฒซ ๋ฒ์งธ ํ์ secondStageName์ ๊ทธ๋ฃน ํค๋๋ก ์ฌ์ฉ const firstRow = table.getRowModel().rows[0]?.original - const stageName = firstRow?.secondStageName || "2์ฐจ ์คํ
์ด์ง" + const stageName = firstRow?.secondStageName || "Second Stage" return ( <div className="text-center font-medium text-gray-700"> {stageName} @@ -195,27 +195,27 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "secondStagePlanDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="๊ณํ์ผ" /> + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> ), cell: ({ row }) => { return <SecondStagePlanDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "2์ฐจ ๊ณํ์ผ" + excelHeader: "Second Planned Date" }, }, { accessorKey: "secondStageActualDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="์ค์ ์ผ" /> + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> ), cell: ({ row }) => { return <SecondStageActualDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "2์ฐจ ์ค์ ์ผ" + excelHeader: "Second Actual Date" }, }, ], @@ -225,7 +225,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "attachmentCount", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="ํ์ผ" /> + <DataTableColumnHeaderSimple column={column} title="Files" /> ), cell: ({ row }) => { const count = row.original.attachmentCount || 0 @@ -237,7 +237,7 @@ export function getSimplifiedDocumentColumns({ size: 60, enableResizing: true, meta: { - excelHeader: "์ฒจ๋ถํ์ผ" + excelHeader: "Attachments" }, }, @@ -245,7 +245,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "updatedAt", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="์
๋ฐ์ดํธ" /> + <DataTableColumnHeaderSimple column={column} title="Updated" /> ), cell: ({ cell, row }) => { return ( @@ -254,7 +254,7 @@ export function getSimplifiedDocumentColumns({ }, enableResizing: true, meta: { - excelHeader: "์
๋ฐ์ดํธ" + excelHeader: "Updated" }, }, diff --git a/lib/vendor-document-list/ship/enhanced-documents-table.tsx b/lib/vendor-document-list/ship/enhanced-documents-table.tsx index 2354a9be..9885c027 100644 --- a/lib/vendor-document-list/ship/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/ship/enhanced-documents-table.tsx @@ -25,17 +25,17 @@ import { EnhancedDocTableToolbarActions } from "./enhanced-doc-table-toolbar-act const DRAWING_KIND_INFO = { B3: { title: "B3 Vendor", - description: "Approval โ Work ๋จ๊ณ๋ก ์งํ๋๋ ์น์ธ ์ค์ฌ ๋๋ฉด", + description: "Approval-focused drawings progressing through Approval โ Work stages", color: "bg-blue-50 text-blue-700 border-blue-200" }, B4: { title: "B4 GTT", - description: "Pre โ Work ๋จ๊ณ๋ก ์งํ๋๋ DOLCE ์ฐ๋ ๋๋ฉด", + description: "DOLCE-integrated drawings progressing through Pre โ Work stages", color: "bg-green-50 text-green-700 border-green-200" }, B5: { title: "B5 FMEA", - description: "First โ Second ๋จ๊ณ๋ก ์งํ๋๋ ์์ฐจ์ ๋๋ฉด", + description: "Sequential drawings progressing through First โ Second stages", color: "bg-purple-50 text-purple-700 border-purple-200" } } as const @@ -79,22 +79,22 @@ export function SimplifiedDocumentsTable({ const advancedFilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ { id: "docNumber", - label: "๋ฌธ์๋ฒํธ", - type: "text", - }, - { - id: "vendorDocNumber", - label: "๋ฒค๋ ๋ฌธ์๋ฒํธ", + label: "Document No", type: "text", }, + // { + // id: "vendorDocNumber", + // label: "Vendor Document No", + // type: "text", + // }, { id: "title", - label: "๋ฌธ์์ ๋ชฉ", + label: "Document Title", type: "text", }, { id: "drawingKind", - label: "๋ฌธ์์ข
๋ฅ", + label: "Document Type", type: "select", options: [ { label: "B3", value: "B3" }, @@ -104,78 +104,78 @@ export function SimplifiedDocumentsTable({ }, { id: "projectCode", - label: "ํ๋ก์ ํธ ์ฝ๋", + label: "Project Code", type: "text", }, { id: "vendorName", - label: "๋ฒค๋๋ช
", + label: "Vendor Name", type: "text", }, { id: "vendorCode", - label: "๋ฒค๋ ์ฝ๋", + label: "Vendor Code", type: "text", }, { id: "pic", - label: "๋ด๋น์", + label: "PIC", type: "text", }, { id: "status", - label: "๋ฌธ์ ์ํ", + label: "Document Status", type: "select", options: [ - { label: "ํ์ฑ", value: "ACTIVE" }, - { label: "๋นํ์ฑ", value: "INACTIVE" }, - { label: "๋ณด๋ฅ", value: "PENDING" }, - { label: "์๋ฃ", value: "COMPLETED" }, + { label: "Active", value: "ACTIVE" }, + { label: "Inactive", value: "INACTIVE" }, + { label: "Pending", value: "PENDING" }, + { label: "Completed", value: "COMPLETED" }, ], }, { id: "firstStageName", - label: "1์ฐจ ์คํ
์ด์ง", + label: "First Stage", type: "text", }, { id: "secondStageName", - label: "2์ฐจ ์คํ
์ด์ง", + label: "Second Stage", type: "text", }, { id: "firstStagePlanDate", - label: "1์ฐจ ๊ณํ์ผ", + label: "First Planned Date", type: "date", }, { id: "firstStageActualDate", - label: "1์ฐจ ์ค์ ์ผ", + label: "First Actual Date", type: "date", }, { id: "secondStagePlanDate", - label: "2์ฐจ ๊ณํ์ผ", + label: "Second Planned Date", type: "date", }, { id: "secondStageActualDate", - label: "2์ฐจ ์ค์ ์ผ", + label: "Second Actual Date", type: "date", }, { id: "issuedDate", - label: "๋ฐํ์ผ", + label: "Issue Date", type: "date", }, { id: "createdAt", - label: "์์ฑ์ผ", + label: "Created Date", type: "date", }, { id: "updatedAt", - label: "์์ ์ผ", + label: "Updated Date", type: "date", }, ] @@ -184,32 +184,32 @@ export function SimplifiedDocumentsTable({ const b4FilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ { id: "cGbn", - label: "C ๊ตฌ๋ถ", + label: "C Category", type: "text", }, { id: "dGbn", - label: "D ๊ตฌ๋ถ", + label: "D Category", type: "text", }, { id: "degreeGbn", - label: "Degree ๊ตฌ๋ถ", + label: "Degree Category", type: "text", }, { id: "deptGbn", - label: "Dept ๊ตฌ๋ถ", + label: "Dept Category", type: "text", }, { id: "jGbn", - label: "J ๊ตฌ๋ถ", + label: "J Category", type: "text", }, { id: "sGbn", - label: "S ๊ตฌ๋ถ", + label: "S Category", type: "text", }, ] @@ -246,17 +246,17 @@ export function SimplifiedDocumentsTable({ {kindInfo && ( <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> - <Badge variant="default" className="flex items-center gap-1 text-sm"> + {/* <Badge variant="default" className="flex items-center gap-1 text-sm"> <FileText className="w-4 h-4" /> {kindInfo.title} </Badge> <span className="text-sm text-muted-foreground"> {kindInfo.description} - </span> + </span> */} </div> <div className="flex items-center gap-2"> <Badge variant="outline"> - {total}๊ฐ ๋ฌธ์ + {total} documents </Badge> </div> </div> diff --git a/lib/vendor-document-list/ship/import-from-dolce-button.tsx b/lib/vendor-document-list/ship/import-from-dolce-button.tsx index d4728d22..de9e63bc 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -72,7 +72,7 @@ export function ImportFromDOLCEButton({ setVendorContractIds(contractIds) } catch (error) { console.error('Failed to fetch vendor contracts:', error) - toast.error('๊ณ์ฝ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋๋ฐ ์คํจํ์ต๋๋ค.') + toast.error('Failed to fetch contract information.') } finally { setLoadingVendorContracts(false) } @@ -142,7 +142,7 @@ export function ImportFromDOLCEButton({ } catch (error) { console.error('Failed to fetch import statuses:', error) - toast.error('์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ํ๋ก์ ํธ ์ค์ ์ ํ์ธํด์ฃผ์ธ์.') + toast.error('Unable to check status. Please verify project settings.') } finally { setStatusLoading(false) } @@ -230,16 +230,16 @@ export function ImportFromDOLCEButton({ if (totalResult.success) { toast.success( - `DOLCE ๊ฐ์ ธ์ค๊ธฐ ์๋ฃ`, + `DOLCE import completed`, { - description: `์ ๊ท ${totalResult.newCount}๊ฑด, ์
๋ฐ์ดํธ ${totalResult.updatedCount}๊ฑด, ๊ฑด๋๋ ${totalResult.skippedCount}๊ฑด (${contractIds.length}๊ฐ ๊ณ์ฝ)` + description: `New ${totalResult.newCount}, Updated ${totalResult.updatedCount}, Skipped ${totalResult.skippedCount} (${contractIds.length} contracts)` } ) } else { toast.error( - `DOLCE ๊ฐ์ ธ์ค๊ธฐ ๋ถ๋ถ ์คํจ`, + `DOLCE import partially failed`, { - description: '์ผ๋ถ ๊ณ์ฝ์์ ๊ฐ์ ธ์ค๊ธฐ์ ์คํจํ์ต๋๋ค.' + description: 'Some contracts failed to import.' } ) } @@ -252,34 +252,34 @@ export function ImportFromDOLCEButton({ setImportProgress(0) setIsImporting(false) - toast.error('DOLCE ๊ฐ์ ธ์ค๊ธฐ ์คํจ', { - description: error instanceof Error ? error.message : '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' + toast.error('DOLCE import failed', { + description: error instanceof Error ? error.message : 'An unknown error occurred.' }) } } const getStatusBadge = () => { if (loadingVendorContracts) { - return <Badge variant="secondary">๊ณ์ฝ ์ ๋ณด ๋ก๋ฉ ์ค...</Badge> + return <Badge variant="secondary">Loading contract information...</Badge> } if (statusLoading) { - return <Badge variant="secondary">DOLCE ์ฐ๊ฒฐ ํ์ธ ์ค...</Badge> + return <Badge variant="secondary">Checking DOLCE connection...</Badge> } if (importStatusMap.size === 0) { - return <Badge variant="destructive">DOLCE ์ฐ๊ฒฐ ์ค๋ฅ</Badge> + return <Badge variant="destructive">DOLCE Connection Error</Badge> } if (!totalStats.importEnabled) { - return <Badge variant="secondary">DOLCE ๊ฐ์ ธ์ค๊ธฐ ๋นํ์ฑํ</Badge> + return <Badge variant="secondary">DOLCE Import Disabled</Badge> } if (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) { return ( <Badge variant="samsung" className="gap-1"> <AlertTriangle className="w-3 h-3" /> - ์
๋ฐ์ดํธ ๊ฐ๋ฅ ({contractIds.length}๊ฐ ๊ณ์ฝ) + Updates Available ({contractIds.length} contracts) </Badge> ) } @@ -287,7 +287,7 @@ export function ImportFromDOLCEButton({ return ( <Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600"> <CheckCircle className="w-3 h-3" /> - DOLCE์ ๋๊ธฐํ๋จ + Synchronized with DOLCE </Badge> ) } @@ -316,7 +316,7 @@ export function ImportFromDOLCEButton({ ) : ( <Download className="w-4 h-4" /> )} - <span className="hidden sm:inline">DOLCE์์ ๊ฐ์ ธ์ค๊ธฐ</span> + <span className="hidden sm:inline">Import from DOLCE</span> {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( <Badge variant="samsung" @@ -332,9 +332,9 @@ export function ImportFromDOLCEButton({ <PopoverContent className="w-96"> <div className="space-y-4"> <div className="space-y-2"> - <h4 className="font-medium">DOLCE ๊ฐ์ ธ์ค๊ธฐ ์ํ</h4> + <h4 className="font-medium">DOLCE Import Status</h4> <div className="flex items-center justify-between"> - <span className="text-sm text-muted-foreground">ํ์ฌ ์ํ</span> + <span className="text-sm text-muted-foreground">Current Status</span> {getStatusBadge()} </div> </div> @@ -342,15 +342,15 @@ export function ImportFromDOLCEButton({ {/* ๊ณ์ฝ ์์ค ํ์ */} {allDocuments.length === 0 && vendorContractIds.length > 0 && ( <div className="text-xs text-blue-600 bg-blue-50 p-2 rounded"> - ๋ฌธ์๊ฐ ์์ด์ ์ ์ฒด ๊ณ์ฝ์์ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์งํํฉ๋๋ค. + No documents found, importing from all contracts. </div> )} {/* ๋ค์ค ๊ณ์ฝ ์ ๋ณด ํ์ */} {contractIds.length > 1 && ( <div className="text-sm"> - <div className="text-muted-foreground">๋์ ๊ณ์ฝ</div> - <div className="font-medium">{contractIds.length}๊ฐ ๊ณ์ฝ</div> + <div className="text-muted-foreground">Target Contracts</div> + <div className="font-medium">{contractIds.length} contracts</div> <div className="text-xs text-muted-foreground"> Contract IDs: {contractIds.join(', ')} </div> @@ -363,25 +363,25 @@ export function ImportFromDOLCEButton({ <div className="grid grid-cols-2 gap-4 text-sm"> <div> - <div className="text-muted-foreground">์ ๊ท ๋ฌธ์</div> - <div className="font-medium">{totalStats.newDocuments || 0}๊ฑด</div> + <div className="text-muted-foreground">New Documents</div> + <div className="font-medium">{totalStats.newDocuments || 0}</div> </div> <div> - <div className="text-muted-foreground">์
๋ฐ์ดํธ</div> - <div className="font-medium">{totalStats.updatedDocuments || 0}๊ฑด</div> + <div className="text-muted-foreground">Updates</div> + <div className="font-medium">{totalStats.updatedDocuments || 0}</div> </div> </div> <div className="text-sm"> - <div className="text-muted-foreground">DOLCE ์ ์ฒด ๋ฌธ์ (B3/B4/B5)</div> - <div className="font-medium">{totalStats.availableDocuments || 0}๊ฑด</div> + <div className="text-muted-foreground">Total DOLCE Documents (B3/B4/B5)</div> + <div className="font-medium">{totalStats.availableDocuments || 0}</div> </div> {/* ๊ฐ ๊ณ์ฝ๋ณ ์ธ๋ถ ์ ๋ณด (ํผ์น๊ธฐ/์ ๊ธฐ ๊ฐ๋ฅ) */} {contractIds.length > 1 && ( <details className="text-sm"> <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> - ๊ณ์ฝ๋ณ ์ธ๋ถ ์ ๋ณด + Details by Contract </summary> <div className="mt-2 space-y-2 pl-2 border-l-2 border-muted"> {contractIds.map(contractId => { @@ -391,10 +391,10 @@ export function ImportFromDOLCEButton({ <div className="font-medium">Contract {contractId}</div> {status ? ( <div className="text-muted-foreground"> - ์ ๊ท {status.newDocuments}๊ฑด, ์
๋ฐ์ดํธ {status.updatedDocuments}๊ฑด + New {status.newDocuments}, Updates {status.updatedDocuments} </div> ) : ( - <div className="text-destructive">์ํ ํ์ธ ์คํจ</div> + <div className="text-destructive">Status check failed</div> )} </div> ) @@ -417,12 +417,12 @@ export function ImportFromDOLCEButton({ {isImporting ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> - ๊ฐ์ ธ์ค๋ ์ค... + Importing... </> ) : ( <> <Download className="w-4 h-4 mr-2" /> - ์ง๊ธ ๊ฐ์ ธ์ค๊ธฐ + Import Now </> )} </Button> @@ -448,10 +448,10 @@ export function ImportFromDOLCEButton({ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent className="sm:max-w-md"> <DialogHeader> - <DialogTitle>DOLCE์์ ๋ฌธ์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ</DialogTitle> + <DialogTitle>Import Document List from DOLCE</DialogTitle> <DialogDescription> - ์ผ์ฑ์ค๊ณต์
DOLCE ์์คํ
์์ ์ต์ ๋ฌธ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค. - {contractIds.length > 1 && ` (${contractIds.length}๊ฐ ๊ณ์ฝ ๋์)`} + Import the latest document list from Samsung Heavy Industries DOLCE system. + {contractIds.length > 1 && ` (${contractIds.length} contracts targeted)`} </DialogDescription> </DialogHeader> @@ -459,20 +459,20 @@ export function ImportFromDOLCEButton({ {totalStats && ( <div className="rounded-lg border p-4 space-y-3"> <div className="flex items-center justify-between text-sm"> - <span>๊ฐ์ ธ์ฌ ํญ๋ชฉ</span> + <span>Items to Import</span> <span className="font-medium"> - {totalStats.newDocuments + totalStats.updatedDocuments}๊ฑด + {totalStats.newDocuments + totalStats.updatedDocuments} </span> </div> <div className="text-xs text-muted-foreground"> - ์ ๊ท ๋ฌธ์์ ์
๋ฐ์ดํธ๋ ๋ฌธ์๊ฐ ํฌํจ๋ฉ๋๋ค. (B3, B4, B5) + Includes new and updated documents (B3, B4, B5). <br /> - B4 ๋ฌธ์์ ๊ฒฝ์ฐ GTTPreDwg, GTTWorkingDwg ์ด์ ์คํ
์ด์ง๊ฐ ์๋ ์์ฑ๋ฉ๋๋ค. + For B4 documents, GTTPreDwg and GTTWorkingDwg issue stages will be auto-generated. {contractIds.length > 1 && ( <> <br /> - {contractIds.length}๊ฐ ๊ณ์ฝ์์ ์์ฐจ์ ์ผ๋ก ๊ฐ์ ธ์ต๋๋ค. + Will import sequentially from {contractIds.length} contracts. </> )} </div> @@ -480,7 +480,7 @@ export function ImportFromDOLCEButton({ {isImporting && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> - <span>์งํ๋ฅ </span> + <span>Progress</span> <span>{importProgress}%</span> </div> <Progress value={importProgress} className="h-2" /> @@ -495,7 +495,7 @@ export function ImportFromDOLCEButton({ onClick={() => setIsDialogOpen(false)} disabled={isImporting} > - ์ทจ์ + Cancel </Button> <Button onClick={handleImport} @@ -504,12 +504,12 @@ export function ImportFromDOLCEButton({ {isImporting ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> - ๊ฐ์ ธ์ค๋ ์ค... + Importing... </> ) : ( <> <Download className="w-4 h-4 mr-2" /> - ๊ฐ์ ธ์ค๊ธฐ ์์ + Start Import </> )} </Button> diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx index 61893da5..4607c994 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -1,8 +1,8 @@ -// components/sync/send-to-shi-button.tsx (๋ค์ค ๊ณ์ฝ ๋ฒ์ ) +// components/sync/send-to-shi-button.tsx (์ต์ข
์์ฑ ๋ฒ์ ) "use client" import * as React from "react" -import { Send, Loader2, CheckCircle, AlertTriangle, Settings } from "lucide-react" +import { Send, Loader2, CheckCircle, AlertTriangle, Settings, RefreshCw } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -22,7 +22,9 @@ import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" -import { useSyncStatus, useTriggerSync } from "@/hooks/use-sync-status" +import { Alert, AlertDescription } from "@/components/ui/alert" +// โ
์
๋ฐ์ดํธ๋ Hook import +import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync-status" import type { EnhancedDocument } from "@/types/enhanced-documents" interface SendToSHIButtonProps { @@ -31,13 +33,6 @@ interface SendToSHIButtonProps { projectType: "ship" | "plant" } -interface ContractSyncStatus { - contractId: number - syncStatus: any - isLoading: boolean - error: any -} - export function SendToSHIButton({ documents = [], onSyncComplete, @@ -49,75 +44,45 @@ export function SendToSHIButton({ const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP" - // documents์์ contractId ๋ชฉ๋ก ์ถ์ถ + // ๋ฌธ์์์ ์ ํจํ ๊ณ์ฝ ID ๋ชฉ๋ก ์ถ์ถ const documentsContractIds = React.useMemo(() => { - const uniqueIds = [...new Set(documents.map(doc => doc.contractId).filter(Boolean))] + const validIds = documents + .map(doc => doc.contractId) + .filter((id): id is number => typeof id === 'number' && id > 0) + + const uniqueIds = [...new Set(validIds)] return uniqueIds.sort() }, [documents]) - // ๊ฐ contract๋ณ ๋๊ธฐํ ์ํ ์กฐํ - const contractStatuses = React.useMemo(() => { - return documentsContractIds.map(contractId => { - const { - syncStatus, - isLoading, - error, - refetch - } = useSyncStatus(contractId, targetSystem) - - return { - contractId, - syncStatus, - isLoading, - error, - refetch - } - }) - }, [documentsContractIds, targetSystem]) - - const { - triggerSync, - isLoading: isSyncing, - error: syncError - } = useTriggerSync() - - // ์ ์ฒด ํต๊ณ ๊ณ์ฐ - const totalStats = React.useMemo(() => { - let totalPending = 0 - let totalSynced = 0 - let totalFailed = 0 - let hasError = false - let isLoading = false - - contractStatuses.forEach(({ syncStatus, error, isLoading: loading }) => { - if (error) hasError = true - if (loading) isLoading = true - if (syncStatus) { - totalPending += syncStatus.pendingChanges || 0 - totalSynced += syncStatus.syncedChanges || 0 - totalFailed += syncStatus.failedChanges || 0 - } - }) - - return { - totalPending, - totalSynced, - totalFailed, - hasError, - isLoading, - canSync: totalPending > 0 && !hasError - } - }, [contractStatuses]) + // โ
ํด๋ผ์ด์ธํธ ์ ์ฉ Hook ์ฌ์ฉ (์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ํธํ) + const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus( + documentsContractIds, + targetSystem + ) + + const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync() - // ์๋ฌ ์ํ ํ์ + // ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋๋ฒ๊น
์ ๋ณด React.useEffect(() => { - if (totalStats.hasError) { - console.warn('Failed to load sync status for some contracts') + if (process.env.NODE_ENV === 'development') { + console.log('SendToSHIButton Debug Info:', { + documentsContractIds, + totalStats, + contractStatuses: contractStatuses.map(({ contractId, syncStatus, error }) => ({ + contractId, + pendingChanges: syncStatus?.pendingChanges, + hasError: !!error + })) + }) } - }, [totalStats.hasError]) + }, [documentsContractIds, totalStats, contractStatuses]) + // ๋๊ธฐํ ์คํ ํจ์ const handleSync = async () => { - if (documentsContractIds.length === 0) return + if (documentsContractIds.length === 0) { + toast.info('๋๊ธฐํํ ๊ณ์ฝ์ด ์์ต๋๋ค.') + return + } setSyncProgress(0) let successfulSyncs = 0 @@ -127,9 +92,17 @@ export function SendToSHIButton({ const errors: string[] = [] try { - const contractsToSync = contractStatuses.filter( - ({ syncStatus, error }) => !error && syncStatus?.syncEnabled && syncStatus?.pendingChanges > 0 - ) + // ๋๊ธฐํ ๊ฐ๋ฅํ ๊ณ์ฝ๋ค๋ง ํํฐ๋ง + const contractsToSync = contractStatuses.filter(({ syncStatus, error }) => { + if (error) { + console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.contractId} has error:`, error) + return false + } + if (!syncStatus) return false + if (!syncStatus.syncEnabled) return false + if (syncStatus.pendingChanges <= 0) return false + return true + }) if (contractsToSync.length === 0) { toast.info('๋๊ธฐํํ ๋ณ๊ฒฝ์ฌํญ์ด ์์ต๋๋ค.') @@ -137,12 +110,15 @@ export function SendToSHIButton({ return } + console.log(`Starting sync for ${contractsToSync.length} contracts`) + // ๊ฐ contract๋ณ๋ก ์์ฐจ ๋๊ธฐํ for (let i = 0; i < contractsToSync.length; i++) { const { contractId } = contractsToSync[i] setCurrentSyncingContract(contractId) try { + console.log(`Syncing contract ${contractId}...`) const result = await triggerSync({ contractId, targetSystem @@ -151,17 +127,19 @@ export function SendToSHIButton({ if (result?.success) { successfulSyncs++ totalSuccessCount += result.successCount || 0 + console.log(`Contract ${contractId} sync successful:`, result) } else { failedSyncs++ totalFailureCount += result?.failureCount || 0 - if (result?.errors?.[0]) { - errors.push(`Contract ${contractId}: ${result.errors[0]}`) - } + const errorMsg = result?.errors?.[0] || result?.message || 'Unknown sync error' + errors.push(`Contract ${contractId}: ${errorMsg}`) + console.error(`Contract ${contractId} sync failed:`, result) } } catch (error) { failedSyncs++ const errorMessage = error instanceof Error ? error.message : '์ ์ ์๋ ์ค๋ฅ' errors.push(`Contract ${contractId}: ${errorMessage}`) + console.error(`Contract ${contractId} sync exception:`, error) } // ์งํ๋ฅ ์
๋ฐ์ดํธ @@ -170,6 +148,7 @@ export function SendToSHIButton({ setCurrentSyncingContract(null) + // ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ๋ฐ ํ ์คํธ ํ์ setTimeout(() => { setSyncProgress(0) setIsDialogOpen(false) @@ -185,7 +164,7 @@ export function SendToSHIButton({ toast.warning( `๋ถ๋ถ ๋๊ธฐํ ์๋ฃ: ${successfulSyncs}๊ฐ ์ฑ๊ณต, ${failedSyncs}๊ฐ ์คํจ`, { - description: errors[0] || '์ผ๋ถ ๊ณ์ฝ ๋๊ธฐํ์ ์คํจํ์ต๋๋ค.' + description: errors.slice(0, 3).join(', ') + (errors.length > 3 ? ' ์ธ ๋๋ณด๊ธฐ...' : '') } ) } else { @@ -198,7 +177,7 @@ export function SendToSHIButton({ } // ๋ชจ๋ contract ์ํ ๊ฐฑ์ - contractStatuses.forEach(({ refetch }) => refetch?.()) + refetchAll() onSyncComplete?.() }, 500) @@ -206,19 +185,32 @@ export function SendToSHIButton({ setSyncProgress(0) setCurrentSyncingContract(null) + const errorMessage = syncUtils.formatError(error as any) toast.error('๋๊ธฐํ ์คํจ', { - description: error instanceof Error ? error.message : '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' + description: errorMessage }) + console.error('Sync process failed:', error) } } + // ๋๊ธฐํ ์ํ์ ๋ฐ๋ฅธ ๋ฑ์ง ์์ฑ const getSyncStatusBadge = () => { if (totalStats.isLoading) { - return <Badge variant="secondary">ํ์ธ ์ค...</Badge> + return ( + <Badge variant="secondary" className="gap-1"> + <Loader2 className="w-3 h-3 animate-spin" /> + ํ์ธ ์ค... + </Badge> + ) } if (totalStats.hasError) { - return <Badge variant="destructive">์ค๋ฅ</Badge> + return ( + <Badge variant="destructive" className="gap-1"> + <AlertTriangle className="w-3 h-3" /> + ์ฐ๊ฒฐ ์ค๋ฅ + </Badge> + ) } if (documentsContractIds.length === 0) { @@ -246,10 +238,6 @@ export function SendToSHIButton({ return <Badge variant="secondary">๋ณ๊ฒฝ์ฌํญ ์์</Badge> } - const refreshAllStatuses = () => { - contractStatuses.forEach(({ refetch }) => refetch?.()) - } - return ( <> <Popover> @@ -258,7 +246,7 @@ export function SendToSHIButton({ <Button variant="default" size="sm" - className="flex items-center bg-blue-600 hover:bg-blue-700" + className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700" disabled={isSyncing || totalStats.isLoading || documentsContractIds.length === 0} > {isSyncing ? ( @@ -270,7 +258,7 @@ export function SendToSHIButton({ {totalStats.totalPending > 0 && ( <Badge variant="destructive" - className="h-5 w-5 p-0 text-xs flex items-center justify-center" + className="h-5 w-5 p-0 text-xs flex items-center justify-center ml-1" > {totalStats.totalPending} </Badge> @@ -279,33 +267,66 @@ export function SendToSHIButton({ </div> </PopoverTrigger> - <PopoverContent className="w-96"> + <PopoverContent className="w-96" align="end"> <div className="space-y-4"> <div className="space-y-2"> - <h4 className="font-medium">SHI ๋๊ธฐํ ์ํ</h4> + <div className="flex items-center justify-between"> + <h4 className="font-medium">SHI ๋๊ธฐํ ์ํ</h4> + <Button + variant="ghost" + size="sm" + onClick={refetchAll} + disabled={totalStats.isLoading} + className="h-6 w-6 p-0" + > + {totalStats.isLoading ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : ( + <RefreshCw className="w-3 h-3" /> + )} + </Button> + </div> + <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground">์ ์ฒด ์ํ</span> {getSyncStatusBadge()} </div> + <div className="text-xs text-muted-foreground"> - {documentsContractIds.length}๊ฐ ๊ณ์ฝ ๋์ + {documentsContractIds.length}๊ฐ ๊ณ์ฝ ๋์ โข {targetSystem} ์์คํ
</div> </div> + {/* ์๋ฌ ์ํ ํ์ */} + {totalStats.hasError && ( + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + ์ผ๋ถ ๊ณ์ฝ์ ๋๊ธฐํ ์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์. + {process.env.NODE_ENV === 'development' && ( + <div className="text-xs mt-1 font-mono"> + Debug: {contractStatuses.filter(({ error }) => error).length}๊ฐ ๊ณ์ฝ์์ ์ค๋ฅ + </div> + )} + </AlertDescription> + </Alert> + )} + + {/* ์ ์ ์ํ์ผ ๋ ํต๊ณ ํ์ */} {!totalStats.hasError && documentsContractIds.length > 0 && ( <div className="space-y-3"> <Separator /> <div className="grid grid-cols-3 gap-4 text-sm"> - <div> + <div className="text-center"> <div className="text-muted-foreground">๋๊ธฐ ์ค</div> - <div className="font-medium">{totalStats.totalPending}๊ฑด</div> + <div className="font-medium text-orange-600">{totalStats.totalPending}๊ฑด</div> </div> - <div> + <div className="text-center"> <div className="text-muted-foreground">๋๊ธฐํ๋จ</div> - <div className="font-medium">{totalStats.totalSynced}๊ฑด</div> + <div className="font-medium text-green-600">{totalStats.totalSynced}๊ฑด</div> </div> - <div> + <div className="text-center"> <div className="text-muted-foreground">์คํจ</div> <div className="font-medium text-red-600">{totalStats.totalFailed}๊ฑด</div> </div> @@ -319,17 +340,26 @@ export function SendToSHIButton({ <div className="space-y-2"> {contractStatuses.map(({ contractId, syncStatus, isLoading, error }) => ( <div key={contractId} className="flex items-center justify-between text-xs p-2 rounded border"> - <span>Contract {contractId}</span> + <span className="font-medium">Contract {contractId}</span> {isLoading ? ( - <Badge variant="secondary" className="text-xs">๋ก๋ฉ...</Badge> + <Badge variant="secondary" className="text-xs"> + <Loader2 className="w-3 h-3 mr-1 animate-spin" /> + ๋ก๋ฉ... + </Badge> ) : error ? ( - <Badge variant="destructive" className="text-xs">์ค๋ฅ</Badge> - ) : syncStatus?.pendingChanges > 0 ? ( + <Badge variant="destructive" className="text-xs"> + <AlertTriangle className="w-3 h-3 mr-1" /> + ์ค๋ฅ + </Badge> + ) : syncStatus && syncStatus.pendingChanges > 0 ? ( <Badge variant="destructive" className="text-xs"> {syncStatus.pendingChanges}๊ฑด ๋๊ธฐ </Badge> ) : ( - <Badge variant="secondary" className="text-xs">๋๊ธฐํ๋จ</Badge> + <Badge variant="secondary" className="text-xs"> + <CheckCircle className="w-3 h-3 mr-1" /> + ์ต์ + </Badge> )} </div> ))} @@ -340,28 +370,18 @@ export function SendToSHIButton({ </div> )} - {totalStats.hasError && ( - <div className="space-y-2"> - <Separator /> - <div className="text-sm text-red-600"> - <div className="font-medium">์ฐ๊ฒฐ ์ค๋ฅ</div> - <div className="text-xs">์ผ๋ถ ๊ณ์ฝ์ ๋๊ธฐํ ์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.</div> - </div> - </div> - )} - + {/* ๊ณ์ฝ ์ ๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ */} {documentsContractIds.length === 0 && ( - <div className="space-y-2"> - <Separator /> - <div className="text-sm text-muted-foreground"> - <div className="font-medium">๊ณ์ฝ ์ ๋ณด ์์</div> - <div className="text-xs">๋๊ธฐํํ ๋ฌธ์๊ฐ ์์ต๋๋ค.</div> - </div> - </div> + <Alert> + <AlertDescription> + ๋๊ธฐํํ ๋ฌธ์๊ฐ ์์ต๋๋ค. ๋ฌธ์๋ฅผ ์ ํํด์ฃผ์ธ์. + </AlertDescription> + </Alert> )} <Separator /> + {/* ์ก์
๋ฒํผ๋ค */} <div className="flex gap-2"> <Button onClick={() => setIsDialogOpen(true)} @@ -385,8 +405,9 @@ export function SendToSHIButton({ <Button variant="outline" size="sm" - onClick={refreshAllStatuses} + onClick={refetchAll} disabled={totalStats.isLoading} + className="px-3" > {totalStats.isLoading ? ( <Loader2 className="w-4 h-4 animate-spin" /> @@ -403,9 +424,12 @@ export function SendToSHIButton({ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent className="sm:max-w-md"> <DialogHeader> - <DialogTitle>SHI ์์คํ
์ผ๋ก ๋๊ธฐํ</DialogTitle> + <DialogTitle className="flex items-center gap-2"> + <Send className="w-5 h-5" /> + SHI ์์คํ
์ผ๋ก ๋๊ธฐํ + </DialogTitle> <DialogDescription> - {documentsContractIds.length}๊ฐ ๊ณ์ฝ์ ๋ณ๊ฒฝ๋ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ SHI ์์คํ
์ผ๋ก ์ ์กํฉ๋๋ค. + {documentsContractIds.length}๊ฐ ๊ณ์ฝ์ ๋ณ๊ฒฝ๋ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ {targetSystem} ์์คํ
์ผ๋ก ์ ์กํฉ๋๋ค. </DialogDescription> </DialogHeader> @@ -434,7 +458,8 @@ export function SendToSHIButton({ </div> <Progress value={syncProgress} className="h-2" /> {currentSyncingContract && ( - <div className="text-xs text-muted-foreground"> + <div className="text-xs text-muted-foreground flex items-center gap-1"> + <Loader2 className="w-3 h-3 animate-spin" /> ํ์ฌ ์ฒ๋ฆฌ ์ค: Contract {currentSyncingContract} </div> )} @@ -444,19 +469,20 @@ export function SendToSHIButton({ )} {totalStats.hasError && ( - <div className="rounded-lg border border-red-200 p-4"> - <div className="text-sm text-red-600"> - ์ผ๋ถ ๊ณ์ฝ์ ๋๊ธฐํ ์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์. - </div> - </div> + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + ์ผ๋ถ ๊ณ์ฝ์ ๋๊ธฐํ ์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํ๊ณ ๋ค์ ์๋ํด์ฃผ์ธ์. + </AlertDescription> + </Alert> )} {documentsContractIds.length === 0 && ( - <div className="rounded-lg border border-yellow-200 p-4"> - <div className="text-sm text-yellow-700"> + <Alert> + <AlertDescription> ๋๊ธฐํํ ๊ณ์ฝ์ด ์์ต๋๋ค. ๋ฌธ์๋ฅผ ์ ํํด์ฃผ์ธ์. - </div> - </div> + </AlertDescription> + </Alert> )} <div className="flex justify-end gap-2"> |
