Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 4 additions & 16 deletions .github/workflows/feature-push-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,16 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
node-version: '20'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# pnpm project - ignore npm lock file
package-lock.json

# local env files
.env*.local
.env.local
Expand Down
83 changes: 74 additions & 9 deletions app/api/blogs/route.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,90 @@
/**
* ブログ一覧API
*
* microCMSからブログ記事を取得します。
* includeRestrictedパラメータがtrueの場合は年齢検証を実行します。
*
* Query Parameters:
* - limit: number (default: 10) - 取得件数
* - offset: number (default: 0) - スキップ件数
* - orders: string (default: '-publishedAt') - ソート順
* - categoryId: string (optional) - カテゴリID
* - includeRestricted: 'true' | 'false' (default: 'false') - 年齢制限コンテンツを含むか
*/

import { NextRequest, NextResponse } from "next/server";
import { getBlogs } from "@/lib/microcms";
import { verifyAge } from "@/lib/age-verification";

// ISR: 60秒ごとに再検証
export const revalidate = 60;
export const dynamic = "force-dynamic";

/**
* GET /api/blogs
*
* ブログ記事一覧を取得します。
* 年齢制限コンテンツを含む場合は、サーバーサイドで年齢検証を実行します。
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;

const limit = Math.max(1, Number(searchParams.get("limit")) || 10);
const offset = Math.max(0, Number(searchParams.get("offset")) || 0);
const orders = searchParams.get("orders") || "-publishedAt";
const categoryId = searchParams.get("categoryId") || undefined;
const includeRestricted = searchParams.get("includeRestricted") === "true";

try {
const { searchParams } = new URL(request.url);
const limit = Math.max(1, Number(searchParams.get("limit")) || 10);
const offset = Math.max(0, Number(searchParams.get("offset")) || 0);
const orders = searchParams.get("orders") || "-publishedAt";
// 年齢制限コンテンツを含む場合は年齢検証
let filters = "";

if (includeRestricted) {
const ageVerification = await verifyAge();

const data = await getBlogs({
// 成人でない場合はエラー
if (ageVerification.status !== "adult") {
return NextResponse.json(
{ error: "Age verification required" },
{ status: 403 }
);
}

// 成人の場合はカテゴリフィルタのみ適用
if (categoryId) {
filters = `category[equals]${categoryId}`;
}
} else {
// 年齢制限コンテンツを除外
const restrictedFilter = "restricted[not_equals]true";
filters = categoryId
? `${restrictedFilter}[and]category[equals]${categoryId}`
: restrictedFilter;
}

const { contents, totalCount } = await getBlogs({
limit,
offset,
orders,
filters,
fields: [
"id",
"title",
"content",
"thumbnail",
"category.id",
"category.name",
"restricted",
"createdAt",
"publishedAt",
].join(","),
});

return NextResponse.json(data, { status: 200 });
return NextResponse.json({
contents,
totalCount,
hasMore: offset + limit < totalCount,
});
} catch (error) {
console.error("[api/blogs] Failed to fetch blogs:", error);
console.error("[GET /api/blogs] Error:", error);
return NextResponse.json(
{ error: "Failed to fetch blogs" },
{ status: 500 }
Expand Down
10 changes: 5 additions & 5 deletions app/api/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface HistoryItem {
dealName: string; // 案件名
dealId: string; // 案件ID
eventId: string; // イベントID
status: string | null; // ステータス(未確定/成果確定/否認、G列が空の場合はnull)
status: string | null; // ステータス(未確定/確定/否認、G列が空の場合はnull)
statusLabel: string | null; // ステータス表示ラベル(G列が空の場合はnull)
originalReward?: number; // 原始報酬額(参考、成果がある場合)
}
Expand Down Expand Up @@ -87,9 +87,9 @@ export async function GET(_request: NextRequest) {
status = "pending";
statusLabel = "未確定";
originalReward = result?.originalReward;
} else if (normalizedStatus.includes("成果確定") || normalizedStatus.includes("確定") || normalizedStatus.includes("approved")) {
} else if (normalizedStatus.includes("確定") || normalizedStatus.includes("確定") || normalizedStatus.includes("approved")) {
status = "approved";
statusLabel = "成果確定";
statusLabel = "確定";
originalReward = result?.originalReward;
} else if (normalizedStatus.includes("否認") || normalizedStatus.includes("cancelled")) {
status = "cancelled";
Expand All @@ -106,9 +106,9 @@ export async function GET(_request: NextRequest) {
status = "pending";
statusLabel = "未確定";
originalReward = result.originalReward;
} else if (resultStatus.includes("成果確定") || resultStatus.includes("確定") || resultStatus.includes("approved")) {
} else if (resultStatus.includes("確定") || resultStatus.includes("確定") || resultStatus.includes("approved")) {
status = "approved";
statusLabel = "成果確定";
statusLabel = "確定";
originalReward = result.originalReward;
} else if (resultStatus.includes("否認") || resultStatus.includes("cancelled")) {
status = "cancelled";
Expand Down
63 changes: 62 additions & 1 deletion app/api/members/me/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import { getMemberById } from "@/lib/sheets";
import { getMemberById, updateMember } from "@/lib/sheets";
import { updateProfileSchema } from "@/lib/validations/profile";

/**
* 会員情報取得API
Expand Down Expand Up @@ -46,3 +47,63 @@ export async function GET() {
);
}
}

/**
* 会員情報更新API
*
* PUT /api/members/me
*
* セッションからmemberIdを取得し、Google Sheetsの会員情報を更新
* 現在は生年月日のみ更新可能(将来的に他フィールドも追加可能)
*/
export async function PUT(req: Request) {
try {
// 1. セッション認証
const session = await getServerSession(authOptions);

if (!session || !session.user?.memberId) {
return NextResponse.json(
{ error: "認証が必要です" },
{ status: 401 }
);
}

// 2. リクエストボディ検証
const body = await req.json();
const validatedData = updateProfileSchema.parse(body);

// 3. Google Sheets更新
const updatedMember = await updateMember(
session.user.memberId,
validatedData
);

if (!updatedMember) {
return NextResponse.json(
{ error: "会員情報が見つかりません" },
{ status: 404 }
);
}

// 4. 成功レスポンス(パスワードハッシュ除外)
const { passwordHash: _passwordHash, ...memberData } = updatedMember;
void _passwordHash;

return NextResponse.json(memberData, { status: 200 });
} catch (error) {
console.error("Update member info error:", error);

// Zodバリデーションエラー
if (error instanceof Error && error.name === "ZodError") {
return NextResponse.json(
{ error: "入力データが不正です" },
{ status: 400 }
);
}

return NextResponse.json(
{ error: "会員情報更新中にエラーが発生しました" },
{ status: 500 }
);
}
}
16 changes: 16 additions & 0 deletions app/blog/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { formatDate } from "@/lib/utils";
import { extractExcerpt } from "@/lib/blog-utils";
import { BlogContent } from "@/components/blog/blog-content";
import { BackToBlogLink } from "@/components/blog/back-to-blog-link";
import { verifyAge, getAccessDeniedError } from "@/lib/age-verification";
import AccessDeniedMessage from "@/components/age-verification/access-denied-message";
// import { DealCTAButton } from "@/components/deal/deal-cta-button"; // 将来的にGoogle Sheets APIから案件取得時に使用

interface BlogDetailPageProps {
Expand Down Expand Up @@ -90,6 +92,20 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
notFound();
}

// 年齢制限コンテンツの場合は年齢検証
if (blog.restricted) {
const ageVerification = await verifyAge();
const accessError = getAccessDeniedError(ageVerification);

if (accessError) {
return (
<div className="container mx-auto px-4 py-8">
<AccessDeniedMessage error={accessError} />
</div>
);
}
}

const excerpt = extractExcerpt(blog.content, 120);

const articleSchema = {
Expand Down
1 change: 1 addition & 0 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default async function BlogListPage() {
getBlogs({
limit: BLOGS_PER_PAGE,
offset: 0,
filters: "restricted[not_equals]true", // 年齢制限コンテンツを除外
}),
getCategories({ limit: 100 }),
]);
Expand Down
6 changes: 6 additions & 0 deletions app/mypage/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export default function MypageLayout({
>
登録情報
</Link>
<Link
href="/mypage/profile"
className="block px-4 py-2 rounded-md hover:bg-accent transition-colors"
>
プロフィール編集
</Link>
<Link
href="/mypage/history"
className="block px-4 py-2 rounded-md hover:bg-accent transition-colors"
Expand Down
9 changes: 9 additions & 0 deletions app/mypage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ export default function MypagePage() {
>
ブログで最新情報を見る
</Link>
<Link
href="/mypage/profile"
className={cn(
buttonVariants({ size: "lg" }),
"rounded-full border-2 border-white bg-transparent px-8 text-white shadow-lg hover:bg-white/10"
)}
>
登録情報を編集する
</Link>
</div>
</motion.div>
<motion.div variants={fadeUpVariants}>
Expand Down
Loading
Loading