The Source.
이 블로그를 구성하는 실제 소스 코드(Raw Code)의 로직을 탐색합니다.
src / pages
ReadOnly* 파일을 클릭하면 우측 에디터에
실제 로직(Frontmatter)이 로드됩니다.
ASTROindex.astro
Source Viewer---
// src/pages/index.astro
import SubLayout from "../layouts/SubLayout.astro";
import { MAIN_MENU } from "../data/navigation";
import SectionSplit from "../components/SectionSplit.astro";
import SectionCentered from "../components/SectionCentered.astro";
import SectionCta from "../components/SectionCta.astro";
import { team, story } from "../data/aboutData";
import { Icons } from "../components/icons/AboutIcons";
import Badge from "../components/Badge.astro";
import Button from "../components/Button.astro";
import toolsUpdates from "../data/toolsUpdates.json";
------
// src/pages/about/index.astro
import SubLayout from "../../layouts/SubLayout.astro";
import SubPageTitle from "../../components/SubPageTitle.astro";
import SectionCentered from "../../components/SectionCentered.astro";
import SectionCta from "../../components/SectionCta.astro";
import Button from "../../components/Button.astro";
import { MAIN_MENU } from "../../data/navigation";
// Imported Data & Icons
import { team } from "../../data/aboutData";
import { Icons } from "../../components/icons/AboutIcons";
------
// src/pages/origin.astro
import SubLayout from "../layouts/SubLayout.astro";
import SubPageTitle from "../components/SubPageTitle.astro";
import SectionCentered from "../components/SectionCentered.astro";
import SectionCta from "../components/SectionCta.astro";
import SectionSplit from "../components/SectionSplit.astro";
import Button from "../components/Button.astro";
import { MAIN_MENU } from "../data/navigation";
import {
beforeMatcha,
stages,
stageMembers,
stageTitles,
Icons,
} from "../data/originData";
------
// src/pages/recipe.astro
import SubLayout from "../layouts/SubLayout.astro";
import SubPageTitle from "../components/SubPageTitle.astro";
import SectionCentered from "../components/SectionCentered.astro";
import SectionCta from "../components/SectionCta.astro";
import SectionSplit from "../components/SectionSplit.astro";
import Button from "../components/Button.astro";
import { MAIN_MENU } from "../data/navigation";
const types = [
{
type: "A",
name: "Standing Recipe",
korName: "정해진 배합",
badgeClass: "bg-brand-antigravity text-white border-transparent",
themeColor: "text-brand-antigravity border-brand-antigravity",
titleColor: "text-brand-antigravity",
metaphor:
"이미 정해둔 레시피대로 우려냅니다. 매번 재료의 양을 새로 재지 않습니다.",
desc: "미리 세워둔 지침과 과거 기록을 참고해, Antigravity가 스스로 판단하고 실행합니다. 반복적이고 규칙이 선명한 작업에 적합합니다.",
flow: ["지침 참고", "실행"],
},
{
type: "B",
name: "Blend Council",
korName: "사전 블렌딩",
badgeClass: "bg-brand-gemini text-white border-transparent",
themeColor: "text-brand-gemini border-brand-gemini",
titleColor: "text-brand-gemini",
metaphor:
"우려내기 전에 티마스터들이 먼저 모여 배합 비율을 논의하고 확정한 뒤 우려냅니다.",
desc: "Gemini 또는 Claude와 홈페이지 개편 아이디어를 먼저 논의해 구조와 방향을 확정한 뒤, Antigravity에게 구체적으로 지시해 실행합니다.",
flow: ["아이디어 회의", "구체적 지시", "실행"],
},
{
type: "C",
name: "Re-steep",
korName: "재탕형",
badgeClass: "bg-brand-jiyu text-white border-transparent",
themeColor: "text-brand-jiyu border-brand-jiyu",
titleColor: "text-brand-jiyu",
metaphor:
"일단 우려서 맛을 본 뒤, 부족하면 다시 우려내며 조절합니다. 처음부터 완성형만을 목표하지 않습니다.",
desc: "희망하는 디자인 방향만 Antigravity에게 제시하고, 제안된 MVP(최소 기능 제품)를 직접 확인해가며 디테일과 텍스트의 감성을 점진적으로 수정합니다.",
flow: ["방향 제시", "MVP 제작", "확인하며 반복 수정"],
},
];
------
// src/pages/studio/index.astro
import SEO from "../../components/SEO.astro";
import SubHeader from "../../components/SubHeader.astro";
import Footer from "../../components/Footer.astro";
import StudioLoader from "../../components/studio/StudioLoader.astro";
import StudioSlideDeck from "../../components/studio/StudioSlideDeck.astro";
import StudioSectionHeader from "../../components/studio/StudioSectionHeader.astro";
import { STUDIO_HOME_MENU } from "../../data/navigation";
const canonicalURL = new URL(
Astro.url.pathname,
Astro.site || "https://matchalogic.com",
);
const studioTools = [
{
title: "주식 평단가 시뮬레이터",
subtitle: "Stock Average Simulator",
badge: "LIVE SIMULATOR",
badgeColor: "bg-matcha-500 text-white shadow-sm",
description:
"x축 수량, y축 주가/투입금액 듀얼 축 SVG 좌표평면에서 단계별 물타기 매수 시나리오를 동적으로 조작하며 최종 평단가 희석 및 자금 추이를 실시간 시각 분석합니다.",
href: "/studio/stock-average-simulator",
icon: `M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1.5-3m8.25 3l1.5-3m-10.5 0h10.5m-10.5 0L6 9.75M16.5 13.5L18 9.75M6 9.75h12M6 9.75l-1.5-3m15 3l1.5-3`,
isLive: true,
},
{
title: "과제 미루기 한계선과 마감 완수 시뮬레이터",
subtitle: "Procrastination & Deadline Simulator",
badge: "LIVE SIMULATOR",
badgeColor: "bg-rose-50 text-rose-700 border border-rose-200",
description:
"착수일을 미룰 때 발생하는 일일 필요 작업량의 폭증 한계선과, 하루 투자 시간별 마감 완주 일정을 단일 통합 좌표계로 시각화 분석합니다.",
href: "/studio/deadline-crunch-simulator",
icon: `M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z`,
isLive: true,
},
{
title: "새 스튜디오 시뮬레이터 (준비 중)",
subtitle: "Upcoming Simulator",
badge: "COMING SOON",
badgeColor:
"bg-slate-modern-100 text-slate-modern-500 border border-slate-modern-200",
description:
"MatchaLogic Studio에서 선보일 새로운 대화형 시뮬레이터를 준비하고 있습니다.",
href: "#",
icon: `M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 005.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941`,
isLive: false,
},
];
------
import SubLayout from "../../layouts/SubLayout.astro";
import { KITCHEN_MENU } from "../../data/navigation";
import { siteConfig } from "../../data/siteConfig";
import { getCollection, render } from "astro:content";
import Badge from "../../components/Badge.astro";
import SectionCentered from "../../components/SectionCentered.astro";
import SectionSplit from "../../components/SectionSplit.astro";
// updates 데이터 가져오기 및 가공
const allUpdates = await getCollection("updates");
const recentUpdates = allUpdates
.sort((a, b) => {
const dateDiff = b.data.date.valueOf() - a.data.date.valueOf();
if (dateDiff !== 0) return dateDiff;
return b.data.version.localeCompare(a.data.version, undefined, {
numeric: true,
});
})
.slice(0, 5);
const updatesWithContent = await Promise.all(
recentUpdates.map(async (update) => {
const { Content } = await render(update);
return { update, Content };
}),
);
const getDotColor = (type: string) => {
switch (type) {
case "major":
return "bg-matcha-500";
case "feature":
return "bg-brand-gemini";
case "design":
return "bg-rose-600";
case "fix":
return "bg-[#8C6239]";
case "content":
return "bg-brand-jiyu";
default:
return "bg-matcha-500";
}
};
const getBadgeVariant = (type: string) => {
switch (type) {
case "major":
return "matcha";
case "feature":
return "yuzu";
case "design":
return "coral";
case "fix":
return "hojicha";
case "content":
return "matcha";
default:
return "matcha";
}
};
const getBadgeColorClasses = (type: string) => {
switch (type) {
case "major":
return "text-matcha-600 bg-matcha-100";
case "feature":
return "text-brand-gemini bg-brand-gemini/10";
case "design":
return "text-rose-600 bg-rose-50";
case "fix":
return "text-[#8C6239] bg-[#8C6239]/10";
case "content":
return "text-brand-jiyu bg-brand-jiyu/10";
default:
return "text-matcha-600 bg-matcha-100";
}
};
------
// src/pages/kitchen/changelog.astro
import SubLayout from '../../layouts/SubLayout.astro';
import SubPageTitle from '../../components/SubPageTitle.astro';
import SectionCentered from '../../components/SectionCentered.astro';
import { KITCHEN_MENU } from '../../data/navigation';
import Button from '../../components/Button.astro';
import { getCollection, render } from 'astro:content';
import { manualBlogMapping, manualTastingMapping } from '../../data/manualRelations';
import { typeLabels, changeTypeThemes, parseVersion } from '../../data/changelogConfig';
const allUpdates = await getCollection('updates');
const allBlogs = await getCollection('blog');
const allTastings = await getCollection('tasting');
// 파일 이름 기준으로 내림차순(최근 v2.4.0이 맨 위) 정렬
const sortedUpdates = allUpdates.sort((a, b) => {
const versionA = parseVersion(a.id);
const versionB = parseVersion(b.id);
if (versionB[0] !== versionA[0]) return versionB[0] - versionA[0];
if (versionB[1] !== versionA[1]) return versionB[1] - versionA[1];
return versionB[2] - versionA[2];
});
const totalReleases = allUpdates.length;
const existingTypes = Object.keys(typeLabels).filter(t => t !== 'all' && sortedUpdates.some(u => u.data.type === t));
const countByType = sortedUpdates.reduce((acc, u) => {
acc[u.data.type] = (acc[u.data.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// 연동된 블로그 및 테이스팅 노트 매핑 데이터 생성 (1:1 정규화 튜플)
const matrix = await Promise.all(
sortedUpdates.map(async (update) => {
const versionStr = update.data.version;
const { Content } = await render(update);
// 블로그 포스트 연동 탐색 (4.0.0은 /about/scout로 연결)
let matchedBlog = null;
if (versionStr === "4.0.0" || versionStr === "v4.0.0") {
matchedBlog = {
id: "scout",
url: "/about/scout",
data: {
title: "Stage 04: The Scout (ChatGPT 합류와 4.0 업데이트)",
},
};
} else {
const foundBlog = allBlogs.find(blog =>
(update.data.relatedBlog && blog.id === update.data.relatedBlog) ||
(blog.data.relatedUpdate && blog.data.relatedUpdate === versionStr) ||
(manualBlogMapping[versionStr] && blog.id === manualBlogMapping[versionStr])
);
if (foundBlog) {
matchedBlog = {
id: foundBlog.id,
url: `/blog/${foundBlog.id}`,
data: foundBlog.data,
};
}
}
// 테이스팅 노트 연동 탐색 (버전 명세 비교 시 'v' 접두사 정규화)
const cleanVersion = versionStr.replace(/^v/, '');
let matchedTasting = allTastings.find(tasting => {
const cleanTastingVersion = tasting.data.version.replace(/^v/, '');
return cleanTastingVersion === cleanVersion ||
(manualTastingMapping[versionStr] && tasting.id === manualTastingMapping[versionStr]);
});
return {
update,
Content,
blog: matchedBlog,
tasting: matchedTasting
};
})
);
const getTypeBadge = (type: string) => {
return changeTypeThemes[type]?.badge || 'bg-gray-100 text-gray-600';
};
------
// src/pages/kitchen/api.astro
import SubLayout from "../../layouts/SubLayout.astro";
import SubPageTitle from "../../components/SubPageTitle.astro";
import { KITCHEN_MENU } from "../../data/navigation";
import { getCollection } from "astro:content";
import SectionCentered from "../../components/SectionCentered.astro";
import SectionSplit from "../../components/SectionSplit.astro";
import { parseVersion } from "../../data/changelogConfig";
const allPosts = await getCollection("blog");
const sortedPosts = allPosts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
const latestPost = sortedPosts[0];
const allUpdates = await getCollection("updates");
const sortedUpdates = allUpdates.sort((a, b) => {
const versionA = parseVersion(a.data.version || a.id);
const versionB = parseVersion(b.data.version || b.id);
if (versionB[0] !== versionA[0]) return versionB[0] - versionA[0];
if (versionB[1] !== versionA[1]) return versionB[1] - versionA[1];
if (versionB[2] !== versionA[2]) return versionB[2] - versionA[2];
return b.data.date.valueOf() - a.data.date.valueOf();
});
const latestUpdate = sortedUpdates[0];
const endpoints = [
{
method: "GET",
path: "/api/brew/matcha.json",
summary: "블로그 포스팅 통계 조회 API",
description:
"MatchaLogic에 작성된 전체 블로그 포스트의 통계와 함께, 가장 최근 발행된 포스트의 제목, 발행일, 요약 설명을 포함한 메타데이터를 진하고 묵직한 형태로 우려냅니다.",
parameters: [],
response: {
status: 200,
description: "Matcha statistics successfully retrieved.",
example: {
status: 200,
drink: "matcha",
flavor_depth: "Deep & Heavy (100% Full Leaf)",
message:
"진하게 우려낸 MatchaLogic의 핵심 포스팅 통계 데이터입니다. 🍵",
statistics: {
totalPosts: allPosts.length,
latestPost: latestPost
? {
title: latestPost.data.title,
publishDate: latestPost.data.pubDate
.toISOString()
.split("T")[0],
description: latestPost.data.description,
}
: null,
},
},
},
},
{
method: "GET",
path: "/api/brew/sencha.json",
summary: "시스템 업데이트 로그 조회 API",
description:
"MatchaLogic의 전체 시스템 변경 이력(Changelog)의 통계와 함께, 최신 업데이트 기록(버전, 날짜, 유형, 한 줄 요약)을 산뜻하고 투명한 맛으로 정리하여 제공합니다.",
parameters: [],
response: {
status: 200,
description: "Sencha updates log successfully retrieved.",
example: {
status: 200,
drink: "sencha",
flavor_depth: "Light & Refreshing (75% Crisp)",
message: "산뜻하게 정리된 MatchaLogic의 시스템 업데이트 로그입니다. 🍃",
statistics: {
totalUpdates: allUpdates.length,
latestUpdate: latestUpdate
? {
version: latestUpdate.data.version,
date: latestUpdate.data.date.toISOString().split("T")[0],
title: latestUpdate.data.title,
type: latestUpdate.data.type,
}
: null,
},
},
},
},
{
method: "GET",
path: "/api/brew/hojicha.json",
summary: "기술 스택 및 아키텍처 조회 API",
description:
"MatchaLogic의 다실(茶室)을 구동하기 위해 사용된 호스팅, 프레임워크, 스타일링 라이브러리 및 서버리스 기술 명세를 고소하고 아늑한 풍미로 출력합니다.",
parameters: [],
response: {
status: 200,
description: "Hojicha tech stack successfully retrieved.",
example: {
status: 200,
drink: "hojicha",
flavor_depth: "Roasted & Cozy (50% Warm)",
message:
"구수하게 볶아낸 MatchaLogic의 Kitchen 아키텍처 스펙입니다. 🍂",
techStack: {
hosting: "Cloudflare Pages",
styling: "Tailwind CSS",
framework: "Astro v7.0.9",
serverless: "Cloudflare Pages Functions",
language: "TypeScript",
},
},
},
},
{
method: "GET",
path: "/api/brew/coffee.json",
summary: "커피 추출 요청 API",
description:
"서버가 커피 추출 명령을 전달받았을 때의 동작을 확인합니다. 본 서버는 차(Tea) 전용 엔진으로 구성되어 있으므로, 418 I'm a teapot 규격 에러와 관련 메타데이터를 반환합니다.",
parameters: [],
response: {
status: 418,
description: "I'm a teapot (HTTP/1.1 418 I'm a teapot)",
example: {
status: 418,
error: "418 I'm a teapot",
message: "MatchaLogic is a teapot. We only brew tea, not coffee! 🫖",
flavor_depth: "None (0% - Teapot Exception)",
drink: "coffee",
},
},
},
];
const getHeaderTitle = (filename: string) => {
switch (filename) {
case "matcha.json": return "matcha statistics";
case "sencha.json": return "sencha updates log";
case "hojicha.json": return "hojicha tech stack";
case "coffee.json": return "coffee error simulated";
default: return filename;
}
};
------
// src/pages/blog/[...slug].astro
import { getCollection, render } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
import RelatedPosts from "../../components/RelatedPosts.astro";
export async function getStaticPaths() {
const blogEntries = await getCollection("blog");
return blogEntries.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
// Find related update if it exists
let relatedUpdateEntry = null;
if (entry.data.relatedUpdate) {
const updates = await getCollection("updates");
relatedUpdateEntry = updates.find(
(u) => u.data.version === entry.data.relatedUpdate,
);
}
let titleParts = [entry.data.title];
if (entry.data.title.includes("\n")) {
titleParts = entry.data.title.split("\n");
} else if (entry.data.title.includes(":")) {
const index = entry.data.title.indexOf(":");
titleParts = [
entry.data.title.substring(0, index + 1).trim(),
entry.data.title.substring(index + 1).trim(),
];
}
const yyyy = entry.data.pubDate.getFullYear();
const mm = String(entry.data.pubDate.getMonth() + 1).padStart(2, "0");
const dd = String(entry.data.pubDate.getDate()).padStart(2, "0");
const formattedDate = `${yyyy}.${mm}.${dd}`;
---main*Logic Only
UTF-8Astro Core