0%

在實際工作中,我發現同事在拿到名片後,仍然需要經過一連串人工流程:

  • 手機拍照、或是掃描名片
  • 傳到電腦
  • 手動輸入公司、姓名、電話
  • 還很容易漏資料或輸錯

因此我開始思考:
能不能在不架設伺服器、不導入複雜系統的情況下,把這件事自動化?

這篇文章記錄我實作的一套 名片自動建檔系統,核心完全建立在 Google 生態系上。

系統目標與整體概念

我的目標很單純:

只要用手機拍照上傳名片,就能自動解析內容,並寫入 Google Spreadsheet。

整體流程如下:

1.手機直接拍照,上傳到指定的 Google Drive 資料夾

2.由 Google Apps Script 定期掃描新圖片

3.使用 Gemini AI API 辨識名片內容(非單純 OCR),將結構化資料寫入 Google Spreadsheet

4.將處理完成的名片移動到封存資料夾,避免重複處理

使用到的工具與技術

  • Google Drive:名片圖片的儲存與狀態管理
  • Google Sheets:結構化資料表(輕量資料庫)
  • Google Apps Script:自動化中樞
  • Google Gemini Vision API:影像 + 語意理解

這個組合的優點是:
不需要額外主機
成本低
團隊成員幾乎都能上手
維護成本非常小

資料欄位設計

實際使用時,名片資訊往往比想像中複雜(多支電話、分機、圖示標註),
因此我最後採用以下欄位結構:

  • 狀態
  • 公司
  • 姓名
  • 職稱
  • Email
  • Mobile(手機)
  • Office Phone(公司電話 / 分機)
  • 地址
  • 建檔日期
  • 原始檔案網址

這些欄位會在 Google Spreadsheet 的第一列先設定好,後續 Apps Script 會依照固定順序寫入。

第一步:Google Drive 環境準備

先在 Google Drive 建立一個主資料夾,例如:名片管理系統

並在裡面建立兩個子資料夾:

  • Inbox(待處理):
    手機拍照後,名片圖片一律上傳到這裡

  • Archived(已建檔):
    名片處理完成後會自動移動到這個資料夾

重要

請記下這兩個資料夾網址列中 folders/ 後面的 ID,後續 Apps Script 需要使用。

Read more »

You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.

Merge nums1 and nums2 into a single array sorted in non-decreasing order.

The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.

給定兩個整數陣列 nums1nums2,都按照非遞減順序排列,以及兩個整數 mn,分別代表 nums1nums2 中元素的個數。
請將 nums1nums2 合併為一個按非遞減順序排列的陣列。
最終的排序陣列不應該由函數返回,而是要儲存在陣列 nums1 內部。為了實現這一點,nums1 的長度為 m + n,其中前 m 個元素表示應該合併的元素,後 n 個元素設為 0 並且應該被忽略。nums2 的長度為 n

目標:將 nums2 合併於 nums1

  • 必須在 nums1 內部完成合併,不能使用額外的陣列空間
  • 不返回值:函數不需要 return,直接修改 nums1
Read more »

最近在部署專案到 Vercel 時,遇到了一個讓我困擾的錯誤訊息:「Unexpected token ‘<’」。這個錯誤通常表示我們預期收到的是 JSON 格式的資料,但實際上卻收到了 HTML 頁面。本文將分享我遇到這個問題的背景、原因,以及兩種解決方案,幫助大家在使用 Vercel 部署時能夠順利取得 API 資料。

部署至 Vercel 遇到「Unexpected token ‘<’」錯誤:原因與解法

在將專案部署到 Vercel 時,當我透過瀏覽器或 extension background fetch 呼叫 API,卻出現以下錯誤:

1
取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

那代表請求並沒有真正拿到 JSON,而是得到了 HTML 頁面。本文將說明這個問題的原因、背後的背景,以及兩種完整的解法。

問題說明

這次在 Arc 瀏覽器 的開發過程中,我嘗試從部署於 Vercel 的 API 端點取得資料,但 Console 顯示:

1
index.iife_dev.js:24922 取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

進一步到 Arc 的 Service-Worker DevToolsarc://inspect/#service-workers)中,打開 background worker 的 Console,可以看到請求的回應實際上是一份 HTML 登入頁面,而非 JSON 資料。


背景補充:Vercel 的 Deployment Protection

這個錯誤並非來自 CORS,而是因為 Vercel 的 Deployment Protection 機制。
當專案開啟「Vercel Authentication」後,所有 Preview Deployments 都需要登入才能訪問。

因此,extension background 在發出請求時,被導向至登入頁面(HTML),導致解析 JSON 時出現:

Unexpected token '<'

Read more »

題目描述

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

給定兩個已排序鏈結串列的頭節點 list1 和 list2。
將兩個串列合併為一個已排序的串列。該串列應透過拼接前兩個串列的節點來建立。
回傳合併後鏈結串列的頭節點。

Example 1:
Input: list1 = [1,2,4], list2 = [1,3,4]
Output: [1,1,2,3,4,4]

Example 2:
Input: list1 = [], list2 = []
Output: []

Example 3:
Input: list1 = [], list2 = [0]
Output: [0]

解題思路

我一開始想法,會需要 2 迴圈去比較,然後組出一個新的 list node,最後再將新的 list node 回傳。
但是這樣的話,時間複雜度會是 O(n^2),因為每次都要從頭開始比較。
後來想說,因為兩個 list 都是已排序好的,所以可以用雙指標的方式,一次比較兩個 list 的頭節點,將較小的節點加入新的 list node,然後將該指標往後移動一位,這樣時間複雜度就會是 O(n),因為每個節點只會被比較一次。

範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let head = null
let nowNode = null
let list1pointer = list1;
let list2pointer = list2;
// 兩兩比較
if(list1 === null && list2 === null) return head
if(list1 === null && list2 !== null) return list2
if(list1 !== null && list2 === null) return list1


// 決定 head
// 因為當你選擇了 list1pointer 作為頭節點後,你應該:
// 移動 list1pointer 指針:list1pointer = list1pointer.next
// 設定 nowNode 為當前的尾節點:nowNode = head
if(list1pointer.val <= list2pointer.val ){
head = list1pointer
list1pointer = list1pointer.next
}else{
head = list2pointer
list2pointer = list2pointer.next
}
nowNode = head

//進入迴圈檢查
while(list1pointer !== null && list2pointer !== null){
if(list1pointer.val <= list2pointer.val){
nowNode.next = list1pointer
list1pointer = list1pointer.next
}else {
nowNode.next = list2pointer
// 移動指針
list2pointer = list2pointer.next
}
nowNode = nowNode.next;
}
// 最後要再檢查。其中一不為 null
if(list1pointer !== null){
nowNode.next = list1pointer
}else{
nowNode.next = list2pointer
}

return head

};
Read more »

Given an array of meeting time intervals where intervals[i] = [startᵢ, endᵢ], determine if a person could attend all meetings.

給定一個會議時間 intervals 陣列,其中 intervals[i] = [startᵢ, endᵢ],判斷一個人是否能夠參加所有會議。

Example 1:

Input:

intervals = [[0,30],[5,10],[15,20]]

Output:

false

Example 2:

Input:

intervals = [[7,10],[2,4]]

Output:

true

Constraints:

  • 0 <= intervals.length <= 10⁴
  • intervals[i].length == 2
  • 0 <= startᵢ < endᵢ <= 10⁶

解題思路

要判斷一個人是否能夠參加所有會議,關鍵在於檢查會議時間是否有重疊。若有任何兩個會議的時間區間重疊,則無法參加所有會議。

重疊情況分析

[1,5][2,4] 為範例:

  1. 假設會議時間按開始時間排序:將所有會議按照開始時間由早到晚排列
  2. 檢查相鄰會議:排序後,只需檢查相鄰的會議是否重疊
  3. 重疊條件:前一個會議的結束時間 > 下一個會議的開始時間

步驟

  1. 將所有會議按開始時間排序
  2. 遍歷排序後的會議陣列
  3. 檢查每個會議的結束時間是否晚於下一個會議的開始時間
  4. 若發現重疊,回傳 false;否則回傳 true

這樣的方法時間複雜度為 O(n log n)(主要是排序的時間),空間複雜度為 O(1)。

Read more »

列表的動態排序和插入為常見的需求。然而,當面對大量資料時,傳統的「刪除重建」策略容易會成為效能瓶頸。本文將分享我在專案中如何透過 SeqNo 管理機制,將列表插入效能提升,同時保持資料一致性和系統穩定性。

在專案內有一下拉列表的功能,當使用者選擇某個選項後,會將該選項插入到列表中,並且會依據使用者的操作順序來進行排序。最初的實作方式是每次插入新選項時,都會重新計算整個列表的排序,這在資料量較大時,導致效能明顯下降。為了解決這個問題,引入了 SeqNo 管理機制。

原有問題

當使用者要在列表中間插入新的資料時,使系統面臨挑戰,並且在沒有加入 seqNo 管理機制前,容易遇到排序衝突的問題。

1
2
3
4
5
6
7
// 原有狀態
[
{ id: 'A', seqNo: 1 },
{ id: 'B', seqNo: 2 }, // 要在 B 後面插入新項目
{ id: 'C', seqNo: 3 },
{ id: 'D', seqNo: 4 }
]

如果直接將新項目設為 seqNo: 3,會與現有的項目 C 發生衝突。

傳統解決方案的困境

直觀的解決方案是重新排序所有項目:

1
2
3
4
5
6
7
8
9
10
// 刪除重建
async function insertListData(afterId: string,
newListData: ListData) {
// 1. 刪除所有後續項目 (N-M 次刪除)
await deleteListDataAfter(afterId);

// 2. 重新建立所有項目 (N-M+1 次建立)
await recreateListDataWithNewSeqNo(newListData,
subsequentListData);
}
  • 操作複雜度: O(2N+1) - 其中 N 為列表總長度
  • 資料庫壓力: 大量刪除和建立操作
  • 交易風險: 操作步驟過多,失敗機率高
  • 併發問題: 長時間鎖定,容易產生競爭條件

SeqNo 管理機制

只更新真正需要變動的項目,將複雜度進行改善,是插入點之後的項目數量。

  1. 排序基礎函式
    確保所有項目能依照 seqNo 正確排序。
1
2
3
4
5
6
7
8
// 排序工具
export function sortListDataBySeqNo(listData: ListData[]): ListData[] {
return [...listData].sort((a, b) => {
const aSeqNo = a.seqNo || 0;
const bSeqNo = b.seqNo || 0;
return aSeqNo - bSeqNo;
});
}
  1. 插入策略計算

只計算「插入點之後」需要調整的項目,並產生更新操作清單。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 export function calculateInsertStrategy(
existingListData: ListData[],
afterListDataId: string
): {
insertSeqNo: number;
affectedListData: ListData[];
updateOperations: Array<{ listDataId: string; newSeqNo: number }>;
} {
const sortedListData = sortListDataBySeqNo(existingListData);
const afterIndex = sortedListData.findIndex(data => data.id === afterListDataId);

if (afterIndex === -1) {
throw new Error('afterListDataId not found');
}

// 關鍵:插入點的 seqNo + 1
const insertSeqNo = sortedListData[afterIndex].seqNo! + 1;
// 只影響插入點之後的項目
const affectedListData = sortedListData.slice(afterIndex + 1);

// 產生更新操作清單
const updateOperations = affectedListData.map(data => ({
listDataId: data.id,
newSeqNo: data.seqNo! + 1
}));

return { insertSeqNo, affectedListData, updateOperations };
}

  1. 交易執行
    確保所有更新操作能在同一個 Transaction 中完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function executeSeqNoUpdates(
transaction: Transaction,
operations: Array<{ listDataId: string; newSeqNo: number }>
): Promise<void> {
for (const operation of operations) {
// 更新每個受影響的項目
const listDataRef = adminDb.collection('listData').doc(operation.listDataId);
transaction.update(listDataRef, {
seqNo: operation.newSeqNo,
updatedAt: new Date()
});
}
}

API 實現範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  // POST /api/v1/listData
export async function POST(req: Request) {
// ... 驗證邏輯

if (afterListDataId) {
try {
// 步驟1: 計算最小更新策略
const { updateOperations, insertSeqNo } =
calculateInsertStrategy(existingListData, afterListDataId);

// 步驟2: Transaction 執行
const result = await adminDb.runTransaction(async (transaction) => {
await executeSeqNoUpdates(transaction, updateOperations);

// 插入新項目
const listDataRef = adminDb.collection('listData').doc();
transaction.set(listDataRef, {
...newListData,
seqNo: insertSeqNo
});

return { id: listDataRef.id, seqNo: insertSeqNo };
});

return NextResponse.json(result, { status: 201 });
} catch (error) {
if (error.message === 'afterListDataId not found') {
return NextResponse.json({ message: 'afterListDataId not found' }, { status: 404 });
}
throw error;
}
}
}

此專案的應用回顧與心得

這個問題最初是在「插入列表中間」時被發現的。
當時經常出現排序錯亂,追查後才發現是因為缺乏 SeqNo 管理機制,導致插入時 seqNo 發生衝突。

一開始的解法是 刪除重建,但這帶來兩個明顯的問題:
1.當資料量大時,對後端資料庫造成極大負擔。
2.使用者操作無法預測,若頻繁在列表中間插入或刪除,效能會快速下降。

引入 SeqNo 管理機制 後:

  • 插入效能明顯提升
  • 保持了資料一致性
  • 系統穩定性也大幅改善
    這樣的設計不僅解決了排序衝突,也大幅減少了資料庫的操作次數,讓整體效能更加穩定可

在開發 SaaS 或工具型應用時,「使用者驗證系統」往往是關鍵基礎設施。這篇文章將分享我如何使用 Next.js(App Router)+ MongoDB + NextAuth,同時支援 Google OAuth 和 自定義帳密登入,並分享專案 mongoDB 資料結構與 API 架構。

MongoDB 資料模型設計

為了支援多元登入方式,我設計了以下幾個集合(Collections):

  1. users - 使用者基本資訊
1
2
3
4
5
6
7
8
{
_id: ObjectId,
email: String,
name: String,
createdAt: Date,
updatedAt: Date
}

  1. authProviders - 自定義驗證方式(帳密)
Read more »

LeetCode 題目多樣,尤其是「二元樹(Binary Tree)」相關題型,容易讓人看了發愣。

本篇文章將教你如何釐清解法邏輯,並將二元樹題型拆解為兩大類解題策略,搭配範例與模板,幫助你用 DFS(深度優先搜尋)更系統地解題

類型一:遍歷型(Traversal Type)

這類題目需要「走過整棵樹的所有節點」,通常是為了計算某種總量,例如:

  • 節點總數
  • 最大深度
  • 所有路徑總和
  • 葉子節點的數量

解題思路

可根據需求使用:

  • 後序遍歷 + 回傳值 return → 適合需要組合子樹結果的情況
  • 前序遍歷 + 狀態變數追蹤 → 適合需要累加或記錄路徑狀態的情況

模板(後序 + return)

1
2
3
4
5
6
function dfs(node) {
if (node === null) return baseValue;
const left = dfs(node.left);
const right = dfs(node.right);
return combine(left, right, node);
}

模板(前序 + 狀態變數)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let result = 0;
let pathDepth = 0;

function traverse(node) {
if (node === null) return;

pathDepth++; // 遞迴深入,深度 +1
if (node.left === null && node.right === null) {
result = Math.max(result, pathDepth); // 若為葉子,更新最大深度
}

traverse(node.left);
traverse(node.right);

pathDepth--; // 回歸
}
Read more »