Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
172939f
first commit
saito-shin-bst Mar 21, 2026
bf77c62
feat : 環境構築
saito-shin-bst Mar 21, 2026
61e057e
fix : github ignore
saito-shin-bst Mar 21, 2026
8c8dd0a
webpackのmode(ビルド時最適化レベル)をproduction
saito-shin-bst Mar 21, 2026
7a486b1
fix : webpackでデフォルトの最適化が行われるように、optimisationを空に変更
saito-shin-bst Mar 21, 2026
1d00070
fix : devtoolでのinline-source-mapをなしに変更
saito-shin-bst Mar 21, 2026
b18b855
remove : chunkFormatを削除してデフォルトの適切なフォーマット(ブラウザ向けなら "array-push")を使用させる…
saito-shin-bst Mar 21, 2026
ddd8a3f
fix : webpack.EnvironmentPluginのNODE=ENVをproductionに変更してdevの余計なコードを除外…
saito-shin-bst Mar 21, 2026
7605056
fix : buildコマンドを修正
saito-shin-bst Mar 21, 2026
bfff033
fix : babel.config.jsでのdevelopmentフラグをNODE_ENVに基づいて設定するように変更
saito-shin-bst Mar 21, 2026
b1c18eb
fix : webpack.config.jsで最適化設定を追加し、全てのチャンクを分割するように変更
saito-shin-bst Mar 21, 2026
ac0d473
feat: Webpack本番最適化とルートレベルのコード分割を導入
saito-shin-bst Mar 21, 2026
2fa5655
fix : optimizationのsplitChunksを削除
saito-shin-bst Mar 21, 2026
98b260c
fix: Babel modules: commonjs を false に変更しコード分割・tree shaking を有効化
saito-shin-bst Mar 21, 2026
c69d50b
fix : fontをwoff2に変更しlinkでpreloadするように変更
saito-shin-bst Mar 21, 2026
4fb562d
feat: Integrate Tailwind CSS into the project
saito-shin-bst Mar 21, 2026
dbf9075
fix : lazy importする対象を思いやつだけに変更
saito-shin-bst Mar 21, 2026
eeae53a
fix: add defer attribute to main.js script for improved loading perfo…
saito-shin-bst Mar 21, 2026
2612128
fix : http通信でのjqueryの使用をやめる
saito-shin-bst Mar 21, 2026
9afad0b
fix : HTTPエラーバグ
saito-shin-bst Mar 21, 2026
c793398
feat : フォントファイルをサブバイナリ化
saito-shin-bst Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,12 @@ $RECYCLE.BIN/
application/upload/**
!application/upload/**/
!application/upload/**/.gitkeep

# Database files
application/server/database.sqlite

# 競技戦略・作業ログ(非公開)
.claude/
performance-issues/
worklog/
docs/architecture.md
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nodejs 24.14.0
pnpm 10.32.1
96 changes: 96 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

会話やドキュメントを書く際は必ず日本語で記載すること。

## プロジェクト概要

Web Speed Hackathon 2026 — 架空のSNS「CaX」のパフォーマンスを改善する競技。Lighthouse スコア(表示900点 + 操作250点 = 1150点満点)で採点される。

## コマンド

```bash
# セットアップ(asdf でバージョン管理)
asdf install
cd application && pnpm install --frozen-lockfile

# ビルド・起動
pnpm run build # Webpack でクライアントビルド
pnpm run start # Express サーバー起動(localhost:3000)

# 型チェック・フォーマット
pnpm run typecheck # 全パッケージの TypeScript 型チェック
pnpm run format # oxlint + oxfmt

# シードデータ
pnpm --filter server run seed:generate
pnpm --filter server run seed:insert

# E2E テスト(VRT)
pnpm --filter e2e exec playwright install chromium # 初回のみ
pnpm --filter e2e run test # ローカル実行(要サーバー起動)
pnpm --filter e2e run test:update # スクリーンショット更新
E2E_BASE_URL=https://example.com pnpm --filter e2e run test # リモート実行
```

## アーキテクチャ

### モノレポ構成(pnpm workspaces)

- `application/client/` — React 19 SPA(Webpack 5、Babel、PostCSS)
- `application/server/` — Express 5 API サーバー(Sequelize + SQLite)
- `application/e2e/` — Playwright E2E テスト(VRT)
- `scoring-tool/` — Lighthouse ベースのスコアリングツール(独立ワークスペース)

### クライアント

- エントリ: `client/src/index.tsx` → Redux Provider + React Router の BrowserRouter でラップ
- ルーティング: `AppContainer.tsx` で React Router v7 を使用(`/`, `/posts/:postId`, `/users/:username`, `/dm`, `/search`, `/terms`, `/crok`)
- 状態管理: Redux は redux-form のみ。データフェッチはカスタムフック(`useInfiniteFetch`, `useWs`, `useFetch`)でコンポーネントローカルに管理
- Webpack: `client/webpack.config.js` — 最適化は意図的に無効化(minimization なし、code splitting なし)。ここがパフォーマンス改善のポイント
- 重いライブラリ: FFmpeg.wasm, ImageMagick Wasm, Web LLM, jQuery, KaTeX, kuromoji, react-syntax-highlighter

### サーバー

- エントリ: `server/src/index.ts` → Sequelize 初期化 → Express + WebSocket 起動
- API: `/api/v1/*` 以下にモジュール分割(auth, user, post, comment, direct_message, search, crok, image, movie, sound, initialize)
- WebSocket: Express Router を拡張した `.ws()` メソッドで DM のリアルタイム通信(eventhub による pub/sub)
- DB: SQLite ファイル(`server/database.sqlite`)。Sequelize モデルは defaultScope で関連を自動 include
- `POST /api/v1/initialize` — DB を初期状態にリセット(E2E テスト・採点時に使用)

### Crok(AIチャット)

- `GET /api/v1/crok?prompt=...` は Server-Sent Events でストリーミング
- SSE プロトコルの変更は禁止。レスポンス内容を SSE 以外で伝達することも禁止

## レギュレーション(重要 — 違反は順位対象外)

### 禁止事項
- **著しい機能落ちやデザイン差異を発生させてはならない** — VRT(Visual Regression Tests)と [手動テスト項目](docs/test_cases.md) の両方をパスすること
- **シードデータの各種 ID を変更してはならない**
- **`fly.toml` を変更してはならない**(Fly.io デプロイ時)
- **`GET /api/v1/crok{?prompt}` の SSE プロトコルを変更してはならない**
- **`crok-response.md` の画面構成に必要な情報を SSE 以外の方法で伝達してはならない**
- **VRT・手動テストを通過させるためだけの悪意あるコードを書いてはならない**

### 必須要件
- **`POST /api/v1/initialize` で DB が初期値にリセットされること** — 採点サーバーは初期データ前提で計測する
- **競技終了後〜順位確定まで、アプリケーションにアクセスできる状態を維持すること**

### 許可されていること
- コード・ファイルはすべて自由に変更可
- API レスポンスの項目追加・削除も可
- 外部サービス(SaaS 等)の利用可(費用は自己負担)

## 採点対象ページ(表示)

ホーム、投稿詳細(テキスト/写真/動画/音声)、DM一覧、DM詳細、検索、利用規約 — 計9ページ

## 採点対象シナリオ(操作)

ユーザー認証、DM送信、検索→結果表示、Crokチャット、投稿(テキスト+メディア)— 計5シナリオ

## API ドキュメント

`application/server/openapi.yaml` に OpenAPI 仕様あり
7 changes: 3 additions & 4 deletions application/client/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ module.exports = {
[
"@babel/preset-env",
{
targets: "ie 11",
corejs: "3",
modules: "commonjs",
targets: "defaults, not ie 11, not dead",
modules: false,
useBuiltIns: false,
},
],
[
"@babel/preset-react",
{
development: true,
development: process.env.NODE_ENV === "development",
runtime: "automatic",
},
],
Expand Down
4 changes: 3 additions & 1 deletion application/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MPL-2.0",
"author": "CyberAgent, Inc.",
"scripts": {
"build": "NODE_ENV=development webpack",
"build": "webpack",
"typecheck": "tsc"
},
"dependencies": {
Expand Down Expand Up @@ -57,6 +57,7 @@
"@babel/preset-env": "7.28.3",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@tailwindcss/postcss": "4.2.2",
"@tsconfig/strictest": "2.0.8",
"@types/bluebird": "3.5.42",
"@types/common-tags": "1.8.4",
Expand All @@ -83,6 +84,7 @@
"postcss-loader": "8.2.0",
"postcss-preset-env": "10.4.0",
"react-markdown": "10.1.0",
"tailwindcss": "4.2.2",
"typescript": "5.9.3",
"webpack": "5.102.1",
"webpack-cli": "6.0.1",
Expand Down
2 changes: 2 additions & 0 deletions application/client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const postcssImport = require("postcss-import");
const tailwindcss = require("@tailwindcss/postcss");
const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
plugins: [
postcssImport(),
tailwindcss(),
postcssPresetEnv({
stage: 3,
}),
Expand Down
114 changes: 82 additions & 32 deletions application/client/src/containers/AppContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { useCallback, useEffect, useId, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useId, useState } from "react";
import { Helmet, HelmetProvider } from "react-helmet";
import { Route, Routes, useLocation, useNavigate } from "react-router";

import { AppPage } from "@web-speed-hackathon-2026/client/src/components/application/AppPage";
import { AuthModalContainer } from "@web-speed-hackathon-2026/client/src/containers/AuthModalContainer";
import { CrokContainer } from "@web-speed-hackathon-2026/client/src/containers/CrokContainer";
import { DirectMessageContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer";
import { DirectMessageListContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer";
import { NewPostModalContainer } from "@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer";
import { NotFoundContainer } from "@web-speed-hackathon-2026/client/src/containers/NotFoundContainer";
import { PostContainer } from "@web-speed-hackathon-2026/client/src/containers/PostContainer";
import { SearchContainer } from "@web-speed-hackathon-2026/client/src/containers/SearchContainer";
import { TermContainer } from "@web-speed-hackathon-2026/client/src/containers/TermContainer";
import { TimelineContainer } from "@web-speed-hackathon-2026/client/src/containers/TimelineContainer";
import { UserProfileContainer } from "@web-speed-hackathon-2026/client/src/containers/UserProfileContainer";
import { fetchJSON, sendJSON } from "@web-speed-hackathon-2026/client/src/utils/fetchers";
import {
fetchJSON,
sendJSON,
} from "@web-speed-hackathon-2026/client/src/utils/fetchers";
const CrokContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/CrokContainer").then(
(m) => ({ default: m.CrokContainer }),
),
);
const DirectMessageContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer").then(
(m) => ({ default: m.DirectMessageContainer }),
),
);
const DirectMessageListContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer").then(
(m) => ({ default: m.DirectMessageListContainer }),
),
);
const NewPostModalContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer").then(
(m) => ({ default: m.NewPostModalContainer }),
),
);
const PostContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/PostContainer").then(
(m) => ({ default: m.PostContainer }),
),
);
const SearchContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/SearchContainer").then(
(m) => ({ default: m.SearchContainer }),
),
);
const TimelineContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/TimelineContainer").then(
(m) => ({ default: m.TimelineContainer }),
),
);
const UserProfileContainer = lazy(() =>
import("@web-speed-hackathon-2026/client/src/containers/UserProfileContainer").then(
(m) => ({ default: m.UserProfileContainer }),
),
);

export const AppContainer = () => {
const { pathname } = useLocation();
Expand Down Expand Up @@ -61,28 +96,43 @@ export const AppContainer = () => {
newPostModalId={newPostModalId}
onLogout={handleLogout}
>
<Routes>
<Route element={<TimelineContainer />} path="/" />
<Route
element={
<DirectMessageListContainer activeUser={activeUser} authModalId={authModalId} />
}
path="/dm"
/>
<Route
element={<DirectMessageContainer activeUser={activeUser} authModalId={authModalId} />}
path="/dm/:conversationId"
/>
<Route element={<SearchContainer />} path="/search" />
<Route element={<UserProfileContainer />} path="/users/:username" />
<Route element={<PostContainer />} path="/posts/:postId" />
<Route element={<TermContainer />} path="/terms" />
<Route
element={<CrokContainer activeUser={activeUser} authModalId={authModalId} />}
path="/crok"
/>
<Route element={<NotFoundContainer />} path="*" />
</Routes>
<Suspense>
<Routes>
<Route element={<TimelineContainer />} path="/" />
<Route
element={
<DirectMessageListContainer
activeUser={activeUser}
authModalId={authModalId}
/>
}
path="/dm"
/>
<Route
element={
<DirectMessageContainer
activeUser={activeUser}
authModalId={authModalId}
/>
}
path="/dm/:conversationId"
/>
<Route element={<SearchContainer />} path="/search" />
<Route element={<UserProfileContainer />} path="/users/:username" />
<Route element={<PostContainer />} path="/posts/:postId" />
<Route element={<TermContainer />} path="/terms" />
<Route
element={
<CrokContainer
activeUser={activeUser}
authModalId={authModalId}
/>
}
path="/crok"
/>
<Route element={<NotFoundContainer />} path="*" />
</Routes>
</Suspense>
</AppPage>

<AuthModalContainer id={authModalId} onUpdateActiveUser={setActiveUser} />
Expand Down
Loading
Loading