Skip to content
Draft
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
74 changes: 48 additions & 26 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,9 @@
</script>

<!-- SEO Meta Tags -->
<meta name="description" content="편리한 회고 작성부터 AI 분석까지 Layer에서 함께해요!" />
<meta
name="keywords"
content="회고, 회고록, retrospect, 회고 서비스, AI 분석, 템플릿, KPT, 5F, 성장, 팀 회고, 개인 회고, Layer, 레이어, 성장하는 사람들의 회고 모임, 메모어"
/>
<meta name="description" content="회고 작성부터 AI 분석까지, Layer에서 KPT·5F 등 다양한 템플릿으로 개인·팀 회고를 시작해보세요." />
<meta name="author" content="Layer Team" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://www.layerapp.io/" />

<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes" />
Expand All @@ -46,10 +41,11 @@

<!-- Open Graph Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.layerapp.io/" />
<meta property="og:url" content="https://layerapp.io/" />
<meta property="og:title" content="성장하는 당신을 위한 회고 서비스, Layer" />
<meta property="og:description" content="편리한 회고 작성부터 AI 분석까지 Layer에서 함께해요!" />
<meta property="og:description" content="회고 작성부터 AI 분석까지, Layer에서 KPT·5F 등 다양한 템플릿으로 개인·팀 회고를 시작해보세요." />
<meta property="og:image" content="https://kr.object.ncloudstorage.com/layer-bucket/og-image.png" />
<meta property="og:image:alt" content="Layer — 성장하는 당신을 위한 회고 서비스" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="layer" />
Expand All @@ -58,27 +54,53 @@
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="성장하는 당신을 위한 회고 서비스, Layer" />
<meta name="twitter:description" content="편리한 회고 작성부터 AI 분석까지 Layer에서 함께해요!" />
<meta name="twitter:description" content="회고 작성부터 AI 분석까지, Layer에서 KPT·5F 등 다양한 템플릿으로 개인·팀 회고를 시작해보세요." />
<meta name="twitter:image" content="https://kr.object.ncloudstorage.com/layer-bucket/og-image.png" />
<meta name="twitter:image:alt" content="Layer — 성장하는 당신을 위한 회고 서비스" />

<!-- Open Graph Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.layerapp.io/" />
<meta property="og:title" content="성장하는 당신을 위한 회고 서비스, Layer" />
<meta property="og:description" content="편리한 회고 작성부터 AI 분석까지 Layer에서 함께해요!" />
<meta property="og:image" content="https://kr.object.ncloudstorage.com/layer-bucket/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="layer" />
<meta property="og:locale" content="ko_KR" />

<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="성장하는 당신을 위한 회고 서비스, Layer" />
<meta name="twitter:description" content="편리한 회고 작성부터 AI 분석까지 Layer에서 함께해요!" />
<meta name="twitter:image" content="https://kr.object.ncloudstorage.com/layer-bucket/og-image.png" />
<!-- Structured Data: Organization & WebSite -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://layerapp.io/#organization",
"name": "Layer",
"url": "https://layerapp.io/",
"logo": {
"@type": "ImageObject",
"url": "https://kr.object.ncloudstorage.com/layer-bucket/og-image.png",
"width": 1200,
"height": 630
},
"description": "회고 작성부터 AI 분석까지, Layer에서 KPT·5F 등 다양한 템플릿으로 개인·팀 회고를 시작해보세요.",
"sameAs": []
},
{
"@type": "WebSite",
"@id": "https://layerapp.io/#website",
"url": "https://layerapp.io/",
"name": "Layer",
"inLanguage": "ko-KR",
"publisher": { "@id": "https://layerapp.io/#organization" }
},
{
"@type": "SoftwareApplication",
"name": "Layer",
"operatingSystem": "Web, iOS, Android",
"applicationCategory": "ProductivityApplication",
"url": "https://layerapp.io/",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "KRW"
}
}
]
}
</script>

<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
<script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
</head>
Expand Down
27 changes: 26 additions & 1 deletion apps/web/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# 자동 생성됨 — 수정 시 apps/web/seo.config.cjs의 ROBOTS_DISALLOW_PREFIXES를 변경하세요.

User-agent: *
Allow: /
sitemap: https://layerapp.io/sitemap.xml

Disallow: /myinfo
Disallow: /write
Disallow: /retrospect/new
Disallow: /retrospect/recommend
Disallow: /retrospect/analysis
Disallow: /retrospect/complete
Disallow: /space/create
Disallow: /space/edit/
Disallow: /setnickname/
Disallow: /goals
Disallow: /analysis
Disallow: /api/
Disallow: /staging
Disallow: /desktop/myinfo
Disallow: /desktop/write
Disallow: /desktop/retrospect
Disallow: /desktop/space/create
Disallow: /desktop/space/edit/
Disallow: /desktop/setnickname/
Disallow: /desktop/goals
Disallow: /desktop/analysis

Sitemap: https://layerapp.io/sitemap.xml
128 changes: 128 additions & 0 deletions apps/web/routes.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* 라우트 경로 단일 소스 (Single Source of Truth).
*
* 사용처:
* - src/router/index.tsx → React Router 경로 정의
* - server/server.cjs → soft 404 방지 정규식 매칭
* - seo.config.cjs → INDEXABLE_ROUTES 등의 SEO 정책 derive
*
* 동적 파라미터는 React Router 표기법(`:param`)을 사용하며,
* `routeToRegex`가 자동으로 `[^/]+`로 변환합니다.
*/

/**
* 명명 라우트 상수 (절대 경로 형태).
* 라우터에서 자식 경로로 사용할 때는 `toChildPath()`로 앞 슬래시를 제거합니다.
*/
const ROUTES = {
// ── 공통 / 모바일 (mobile 레이아웃에서 reachable) ─────
ROOT: "/",
LOGIN: "/login",
TEMPLATE: "/template",
STAGING: "/staging",
ANALYSIS: "/analysis",

WRITE: "/write",
WRITE_COMPLETE: "/write/complete",

GOALS: "/goals",
GOALS_MORE: "/goals/more",
GOALS_EDIT: "/goals/edit",

SETNICKNAME: "/setnickname/:socialType",

// 스페이스
SPACE_CREATE: "/space/create",
SPACE_CREATE_DONE: "/space/create/done",
SPACE_CREATE_NEXT: "/space/create/next",
SPACE_EDIT: "/space/edit/:id",
SPACE_VIEW: "/space/:spaceId",
SPACE_TEMPLATES: "/space/:spaceId/templates",
SPACE_MEMBERS: "/space/:spaceId/members",
SPACE_MEMBERS_EDIT: "/space/:spaceId/members/edit",
SPACE_JOIN: "/space/join/:id",

// 회고
RETROSPECT_NEW: "/retrospect/new",
RETROSPECT_COMPLETE: "/retrospect/complete",
RETROSPECT_ANALYSIS: "/retrospect/analysis",
RETROSPECT_WRITE: "/retrospect/write", // 데스크탑 레이아웃에서만 마운트되지만 상수로 명명
RETROSPECT_RECOMMEND: "/retrospect/recommend",
RETROSPECT_RECOMMEND_SEARCH: "/retrospect/recommend/search",
RETROSPECT_RECOMMEND_DONE: "/retrospect/recommend/done",

// 내 정보
MYINFO: "/myinfo",
MYINFO_MODIFY: "/myinfo/modify",
MYINFO_USERDELETION: "/myinfo/userdeletion",
MYINFO_NOTICES: "/myinfo/notices",
MYINFO_HELP: "/myinfo/help",
MYINFO_LICENSE: "/myinfo/license",
MYINFO_TERMSOFSERVICE: "/myinfo/termsofservice",
MYINFO_PRIVACYPOLICY: "/myinfo/privacypolicy",
MYINFO_FEEDBACK: "/myinfo/feedback",

// OAuth
OAUTH_KAKAO: "/api/auth/oauth2/kakao",
OAUTH_GOOGLE: "/api/auth/oauth2/google",
};

/**
* `/desktop` 접두사가 붙어 reachable한 라우트.
*
* 동일한 상대 경로가 router/index.tsx의 deviceSpecificRoutes에서
* `deviceType: "desktop"`으로 정의되어 있어 `/desktop` 마운트 포인트 아래에
* 추가로 노출됩니다.
*
* 라우터에서 `deviceType: "desktop"` 라우트를 추가/제거할 때 이 목록도 갱신하세요.
*/
const DESKTOP_PATHS = [
"/desktop",
"/desktop/login",
"/desktop/goals",
"/desktop/space/:spaceId",
"/desktop/retrospect/analysis",
"/desktop/retrospect/write",
"/desktop/setnickname/:socialType",
];

/** 모든 reachable한 절대 경로 — server.cjs의 soft 404 화이트리스트. */
const ALL_ABSOLUTE_PATHS = [...Object.values(ROUTES), ...DESKTOP_PATHS];

/**
* React Router 경로 → 정규식.
* - `:param` → `[^/]+`
* - 정규식 메타문자는 이스케이프
* - 정확 매칭 (`^...$`)
*/
function routeToRegex(routePath) {
const escaped = routePath
.replace(/[.+*?^${}()|[\]\\]/g, "\\$&")
.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, "[^/]+");
return new RegExp(`^${escaped}$`);
}

/**
* 절대 경로 → React Router 자식 경로 형식 (앞의 "/" 제거).
*
* React Router v6의 nested route 패턴에서 자식 경로는 부모 경로 기준 상대 형식이어야 합니다.
* 예) 부모 `path: "/"` + 자식 `path: "login"` → 최종 URL `/login`
*
* @example toChildPath(ROUTES.LOGIN) // "/login" → "login"
* @example toChildPath(ROUTES.ROOT) // "/" → ""
*/
function toChildPath(routePath) {
return routePath.startsWith("/") ? routePath.slice(1) : routePath;
}

/** server.cjs용 soft 404 방지 정규식 배열. */
const KNOWN_ROUTE_PATTERNS = ALL_ABSOLUTE_PATHS.map(routeToRegex);

module.exports = {
ROUTES,
DESKTOP_PATHS,
ALL_ABSOLUTE_PATHS,
KNOWN_ROUTE_PATTERNS,
routeToRegex,
toChildPath,
};
82 changes: 82 additions & 0 deletions apps/web/routes.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Type declarations for `./routes.cjs`.
*
* `.cjs` 모듈을 .ts/.tsx에서 import할 때 TypeScript가 타입을 추론할 수 있도록 제공합니다.
* 런타임 동작은 `routes.cjs`에 정의되어 있으며, 이 파일은 시그니처만 선언합니다.
*
* 라우트 추가 시 두 곳을 함께 갱신하세요:
* 1. `routes.cjs`의 `ROUTES` 객체
* 2. 이 파일의 `ROUTES` 인터페이스 (IDE 자동완성 + 타입 안전성)
*/

/**
* 명명 라우트 상수 (절대 경로 형태).
* 라우터에서 자식 경로로 사용할 때는 `toChildPath()`로 앞 슬래시를 제거합니다.
*/
export declare const ROUTES: {
readonly ROOT: "/";
readonly LOGIN: "/login";
readonly TEMPLATE: "/template";
readonly STAGING: "/staging";
readonly ANALYSIS: "/analysis";

readonly WRITE: "/write";
readonly WRITE_COMPLETE: "/write/complete";

readonly GOALS: "/goals";
readonly GOALS_MORE: "/goals/more";
readonly GOALS_EDIT: "/goals/edit";

readonly SETNICKNAME: "/setnickname/:socialType";

readonly SPACE_CREATE: "/space/create";
readonly SPACE_CREATE_DONE: "/space/create/done";
readonly SPACE_CREATE_NEXT: "/space/create/next";
readonly SPACE_EDIT: "/space/edit/:id";
readonly SPACE_VIEW: "/space/:spaceId";
readonly SPACE_TEMPLATES: "/space/:spaceId/templates";
readonly SPACE_MEMBERS: "/space/:spaceId/members";
readonly SPACE_MEMBERS_EDIT: "/space/:spaceId/members/edit";
readonly SPACE_JOIN: "/space/join/:id";

readonly RETROSPECT_NEW: "/retrospect/new";
readonly RETROSPECT_COMPLETE: "/retrospect/complete";
readonly RETROSPECT_ANALYSIS: "/retrospect/analysis";
readonly RETROSPECT_WRITE: "/retrospect/write";
readonly RETROSPECT_RECOMMEND: "/retrospect/recommend";
readonly RETROSPECT_RECOMMEND_SEARCH: "/retrospect/recommend/search";
readonly RETROSPECT_RECOMMEND_DONE: "/retrospect/recommend/done";

readonly MYINFO: "/myinfo";
readonly MYINFO_MODIFY: "/myinfo/modify";
readonly MYINFO_USERDELETION: "/myinfo/userdeletion";
readonly MYINFO_NOTICES: "/myinfo/notices";
readonly MYINFO_HELP: "/myinfo/help";
readonly MYINFO_LICENSE: "/myinfo/license";
readonly MYINFO_TERMSOFSERVICE: "/myinfo/termsofservice";
readonly MYINFO_PRIVACYPOLICY: "/myinfo/privacypolicy";
readonly MYINFO_FEEDBACK: "/myinfo/feedback";

readonly OAUTH_KAKAO: "/api/auth/oauth2/kakao";
readonly OAUTH_GOOGLE: "/api/auth/oauth2/google";
};

/** `/desktop` 접두사가 붙어 reachable한 라우트. */
export declare const DESKTOP_PATHS: string[];

/** 모든 reachable한 절대 경로. */
export declare const ALL_ABSOLUTE_PATHS: string[];

/** server.cjs용 soft 404 방지 정규식 배열. */
export declare const KNOWN_ROUTE_PATTERNS: RegExp[];

/** React Router 경로 → 정규식 변환. */
export declare function routeToRegex(routePath: string): RegExp;

/**
* 절대 경로 → React Router 자식 경로 형식 (앞의 "/" 제거).
*
* @example toChildPath("/login") // "login"
* @example toChildPath("/") // ""
*/
export declare function toChildPath(routePath: string): string;
Loading
Loading