
給与確認アプリのリデザイン
実際のユーザーインタビューとフィードバックをもとに、給与確認体験を調査・再設計しました。
ハッカソンで制作した初期版は、構造より開発速度を優先したため、UI・状態管理・APIロジックが密接に結びつき、修正や拡張が難しい状態でした。この再開発を通して関心の分離の重要性を学び、保守しやすく拡張可能なAIダッシュボードへ再構築しました。
このプロジェクトは、デザイナーと参加したハッカソンで制作したものです。短期間での完成を優先した結果、初期実装にはコード構成と状態管理の課題が残りました。


その後、デザインとアーキテクチャの両面からプロジェクトを見直しました。UIを再現するだけでなく、複雑な状態と操作を管理できるよう構成を整理し、拡張可能なフロントエンドへ再構築しました。
ハッカソン版では、動作するAIチャットを短時間で完成させることを優先しました。その結果、UI表示、状態管理、送信処理、API通信が1つのコンポーネントに集中していました。

試作としては機能していましたが、各処理の責任範囲が曖昧で、関心を分離できていませんでした。API通信、ローカル状態、イベント処理、UI表示が密接に結びつき、コードの理解・保守・拡張が難しい構成でした。
初期コードを簡略化した例 ↓
// API communication
const fetchWithRetry = async (payload) => { ... };
// Client state
const [messages, setMessages] = useState(initialMessages);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Business logic
const sendMessage = async (userMessage) => {
if (!userMessage.trim() || isLoading) return;
setMessages(...);
setInput("");
setIsLoading(true);
try {
const result = await fetchWithRetry({ ... });
setMessages(...);
} catch (error) {
setMessages(...);
} finally {
setIsLoading(false);
}
};
const handleSend = (e) => { ... };
const handleQuickAction = (actionText) => { ... };
// View
return (
<section>
{messages.map(...)}
<button ...>...</button>
<form ...>
<input ... />
<button ...>...</button>
</form>
</section>
);チャット機能を拡張しやすくするため、まず各層の責任範囲を明確にしました。ファイルの大きさや画面単位ではなく、View、Logic、Service、Stateという役割ごとにコードを整理しました。

export default function ChatRoomPage() {
const params = useParams();
const chatId = params.chatId as string;
const room = useChatStore((state) => state.rooms[chatId]);
const { isAiThinking, handleSubmit } = useChatRoomController({
chatId,
room,
});
if (!room) return <div>Room not found</div>;
return (
<div>
<h1>{room.title}</h1>
<ScrollableChat messages={room.messages} isAiThinking={isAiThinking} />
<ChatRoomInput handleSubmit={handleSubmit} />
</div>
);
}
export function useChatRoomController({ chatId, room }: Props) {
const addMessage = useChatStore((state) => state.addMessage);
const [isAiThinking, setIsAiThinking] = useState(false);
const hasRequestedInitialResponseRef = useRef(false);
useEffect(() => {
if (!room) return;
if (room.messages.length > 1) return;
if (hasRequestedInitialResponseRef.current) return;
hasRequestedInitialResponseRef.current = true;
const initialAiRes = async () => {
setIsAiThinking(true);
try {
const aiText = await callAi(room.messages);
if (aiText) addMessage(chatId, "assistant", aiText);
} finally {
setIsAiThinking(false);
}
};
initialAiRes();
}, [room, chatId, addMessage]);
const handleSubmit = async (message: string) => {
if (!room) return;
addMessage(chatId, "user", message);
const historyWithNewMessage = [
...room.messages,
{ role: "user", content: message },
];
setIsAiThinking(true);
try {
const aiText = await callAi(historyWithNewMessage);
if (aiText) addMessage(chatId, "assistant", aiText);
} finally {
setIsAiThinking(false);
}
};
return { isAiThinking, handleSubmit };
}
export const callAi = async (messages: LocalChatMessage[]) => {
const chat = window?.puter?.ai?.chat;
if (typeof chat !== "function") return "";
try {
const res = await chat({
messages: withSystemPrompt(messages),
});
return extractAiText(res);
} catch {
const prompt = buildPromptWithSystem(messages);
const res = await chat(prompt);
return extractAiText(res);
}
};
type ChatStore = {
rooms: Record<string, ChatRoom>;
createRoom: (initialMessage: string) => string;
addMessage: (
chatId: string,
role: "user" | "assistant",
content: string
) => void;
};
export const useChatStore = create<ChatStore>((set) => ({
rooms: {},
createRoom: (initialMessage) => {
const id = crypto.randomUUID();
set((state) => ({
rooms: {
...state.rooms,
[id]: {
title: initialMessage,
messages: [{ role: "user", content: initialMessage }],
},
},
}));
return id;
},
addMessage: (chatId, role, content) => {
set((state) => ({
rooms: {
...state.rooms,
[chatId]: {
...state.rooms[chatId],
messages: [
...state.rooms[chatId].messages,
{ role, content },
],
},
},
}));
},
}));複数のチャットルームに対応するにつれ、会話データを1つのコンポーネントの外でも共有する必要が生まれました。ローカル状態だけでは管理が難しくなったため、共有データをZustandのストアへ移しました。
ハッカソンでは、まず動かすことを優先し、構造を設計する重要性を十分に理解できていませんでした。このプロジェクトを見直す中で、設計への理解とAIの使い方が大きく変わりました。AIに「とりあえず動くもの」を作らせるのではなく、明確な設計意図を持ち、判断を支える道具として使うようになりました。AIは短時間でコードを生成できますが、読みやすく保守・拡張できるコードを書くには、土台となるアーキテクチャを自分で理解する必要があります。構造を整理しながら設計すること自体の面白さも実感しました。
このプロジェクトを通して、デザインと実装の間にある差についても考え直しました。Figmaでワイヤーフレームやスタイルガイドを整理しても、一貫したUIとして正確に再現するには、実装時の細かな判断が欠かせません。デザインと実装をつなぐ工程はまだ完全には自動化できず、人の目による調整と判断が重要だと改めて感じました。単なるリファクタリングにとどまらず、AIの活用方法と、デザイン意図を実装へ反映する方法を見直す機会になりました。