Skip to content

Commit 55658cc

Browse files
authored
Merge pull request #93 from SWMTheFirstTake/dev
크로스 브라우징 + JSON-LD
2 parents 937206e + 410d6ea commit 55658cc

10 files changed

Lines changed: 469 additions & 51 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,29 @@ LLM 기반의 대화형 인터페이스를 통해 사용자의 체형, 취향, T
4747

4848
- **Backend**: Spring, PostgreSQL, Redis
4949
- **AI**: FastAPI, LangChain, LangGraph, MongoDB
50+
51+
---
52+
53+
## 🌐 Browser Support
54+
55+
이 프로젝트는 다음 브라우저를 지원합니다:
56+
57+
### **데스크톱 브라우저**
58+
- ✅ Chrome 105+
59+
- ✅ Edge 105+ (Chromium 기반)
60+
- ✅ Firefox 100+
61+
- ✅ Safari 14+
62+
- ✅ Opera 90+
63+
64+
### **모바일 브라우저**
65+
- ✅ iOS Safari 14+
66+
- ✅ Android Chrome 105+
67+
- ✅ Samsung Internet 15+
68+
- ✅ Android Browser 105+
69+
70+
### **지원하지 않는 브라우저**
71+
- ❌ Internet Explorer 11 이하
72+
- ❌ Opera Mini
73+
- ❌ 구형 모바일 브라우저 (시장 점유율 0.2% 미만)
74+
75+
> **참고**: 최신 브라우저 기능을 활용하기 위해 ES2017+ 표준을 사용합니다. 구형 브라우저에서는 일부 기능이 제한될 수 있습니다.

app/chat/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ import { panelAtom, roomIdAtom } from '@/atoms/chatAtoms';
99
import { Suspense, useEffect } from 'react';
1010
import { useRouter } from 'next/navigation';
1111
import { postChatRooms } from '@/api/chatAPI';
12+
import JsonLd from '@/components/seo/JsonLd';
13+
import { createSoftwareApplicationSchema } from '@/lib/schema';
1214

1315
export default function Chat() {
16+
const softwareApplicationSchema = createSoftwareApplicationSchema(
17+
'The First Take - AI 패션 채팅',
18+
'AI 패션 어시스턴트와 실시간으로 대화하며 맞춤형 스타일 추천을 받아보세요.',
19+
'https://the-first-take.com/chat',
20+
'/TFT_icon.png'
21+
);
1422
const panel = useAtomValue(panelAtom);
1523
const setPanel = useSetAtom(panelAtom);
1624
const router = useRouter();
@@ -55,6 +63,7 @@ export default function Chat() {
5563

5664
return (
5765
<Suspense>
66+
<JsonLd data={softwareApplicationSchema} />
5867
<main className="flex flex-col">
5968
{/* 모바일/태블릿에서는 상단에 메뉴 표시 */}
6069
<nav className="lg:hidden">

app/page.tsx

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,77 @@ import Explanation from '@/components/landing/Explanation';
44
import Image from 'next/image';
55
import Features from '@/components/landing/Features';
66
import FAQ from '@/components/landing/FAQ';
7+
import JsonLd from '@/components/seo/JsonLd';
8+
import { createOrganizationSchema, createWebSiteSchema, createSoftwareApplicationSchema } from '@/lib/schema';
79

810
export default function Home() {
11+
const organizationSchema = createOrganizationSchema(
12+
'The First Take',
13+
'https://the-first-take.com',
14+
'/TFT_icon.png',
15+
'AI가 당신만의 완벽한 스타일을 찾아드립니다. 복잡한 옷 고르기, 이제 끝! 상황과 체형을 고려한 딱 한 벌만 추천받으세요.',
16+
);
17+
18+
const webSiteSchema = createWebSiteSchema(
19+
'The First Take',
20+
'https://the-first-take.com',
21+
'AI 패션 스타일 어시스턴트 - 맞춤형 스타일 추천 서비스',
22+
);
23+
24+
const softwareApplicationSchema = createSoftwareApplicationSchema(
25+
'The First Take',
26+
'AI가 당신만의 완벽한 스타일을 찾아드립니다. 복잡한 옷 고르기, 이제 끝! 상황과 체형을 고려한 딱 한 벌만 추천받으세요.',
27+
'https://the-first-take.com',
28+
'/TFT_icon.png',
29+
);
30+
931
return (
10-
<main className="font-sans">
11-
<section className="h-screen flex items-center justify-center relative ">
12-
<div className="absolute inset-x-0 top-0 h-1/2 bg-blue dark:bg-blue-800 -z-20 pointer-events-none">
13-
<Image className="object-cover opacity-60 dark:opacity-30" src={'/logo1.png'} alt={'logo'} fill />
14-
</div>
15-
<div className="absolute inset-0 -z-10 pointer-events-none"></div>
16-
<div className="max-w-5xl mx-auto pb-24 px-4 w-full bg-white dark:bg-slate-800 rounded-4xl shadow-2xl">
17-
<HeroSection />
18-
<NextChat />
19-
</div>
20-
<div className="absolute inset-x-0 bottom-8 flex justify-center">
21-
<a
22-
href="#about"
23-
className="group inline-flex items-center gap-2 text-gray-500 hover:text-gray-900 transition-colors"
24-
>
25-
<span className="text-sm md:text-base">아래로 스크롤하면 제품 설명을 볼 수 있어요</span>
26-
<svg
27-
className="w-5 h-5 transition-transform group-hover:translate-y-0.5"
28-
viewBox="0 0 24 24"
29-
fill="none"
30-
xmlns="http://www.w3.org/2000/svg"
31-
aria-hidden="true"
32+
<>
33+
<JsonLd data={organizationSchema} />
34+
<JsonLd data={webSiteSchema} />
35+
<JsonLd data={softwareApplicationSchema} />
36+
<main className="font-sans">
37+
<section className="h-screen flex items-center justify-center relative ">
38+
<div className="absolute inset-x-0 top-0 h-1/2 bg-blue dark:bg-blue-800 -z-20 pointer-events-none">
39+
<Image className="object-cover opacity-60 dark:opacity-30" src={'/logo1.png'} alt={'logo'} fill />
40+
</div>
41+
<div className="absolute inset-0 -z-10 pointer-events-none"></div>
42+
<div className="max-w-5xl mx-auto pb-24 px-4 w-full bg-white dark:bg-slate-800 rounded-4xl shadow-2xl">
43+
<HeroSection />
44+
<NextChat />
45+
</div>
46+
<div className="absolute inset-x-0 bottom-8 flex justify-center">
47+
<a
48+
href="#about"
49+
className="group inline-flex items-center gap-2 text-gray-500 hover:text-gray-900 transition-colors"
3250
>
33-
<path
34-
d="M6 9l6 6 6-6"
35-
stroke="currentColor"
36-
strokeWidth="2"
37-
strokeLinecap="round"
38-
strokeLinejoin="round"
39-
/>
40-
</svg>
41-
</a>
42-
</div>
43-
</section>
44-
<Explanation />
45-
<Features />
46-
<FAQ />
51+
<span className="text-sm md:text-base">아래로 스크롤하면 제품 설명을 볼 수 있어요</span>
52+
<svg
53+
className="w-5 h-5 transition-transform group-hover:translate-y-0.5"
54+
viewBox="0 0 24 24"
55+
fill="none"
56+
xmlns="http://www.w3.org/2000/svg"
57+
aria-hidden="true"
58+
>
59+
<path
60+
d="M6 9l6 6 6-6"
61+
stroke="currentColor"
62+
strokeWidth="2"
63+
strokeLinecap="round"
64+
strokeLinejoin="round"
65+
/>
66+
</svg>
67+
</a>
68+
</div>
69+
</section>
70+
<Explanation />
71+
<Features />
72+
<FAQ />
4773

48-
<footer className="border-t bg-blue-700 border-border m-8 p-8 text-center text-md text-muted-foreground">
49-
<p>© 2025 the first take. All rights reserved.</p>
50-
</footer>
51-
</main>
74+
<footer className="border-t bg-blue-700 border-border m-8 p-8 text-center text-md text-muted-foreground">
75+
<p>© 2025 the first take. All rights reserved.</p>
76+
</footer>
77+
</main>
78+
</>
5279
);
5380
}

app/signin/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ import SigninSNSForm from '@/components/auth/SigninSNSForm';
22
import LoginBackground from '@/components/auth/LoginBackground';
33
import Image from 'next/image';
44
import LoginBackgroundDark from '@/components/auth/LoginBackgroundDark';
5+
import JsonLd from '@/components/seo/JsonLd';
6+
import { createWebPageSchema } from '@/lib/schema';
57

68
export default function SigninPage() {
9+
const webPageSchema = createWebPageSchema(
10+
'SNS 로그인 - The First Take',
11+
'패션 AI와 함께 나만의 스타일을 찾아보세요. SNS 계정으로 간편하게 로그인하세요.',
12+
'https://the-first-take.com/signin'
13+
);
14+
715
return (
8-
<main className="min-h-screen flex items-center justify-center relative overflow-hidden">
16+
<>
17+
<JsonLd data={webPageSchema} />
18+
<main className="min-h-screen flex items-center justify-center relative overflow-hidden">
919
<LoginBackground />
1020
<LoginBackgroundDark />
1121

@@ -45,5 +55,6 @@ export default function SigninPage() {
4555
</div>
4656
</div>
4757
</main>
58+
</>
4859
);
4960
}

app/wiki/[...slug]/page.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,55 @@
11
import { getAllStaticPaths, getMDXContent } from '@/lib/wikiLoader';
22
import { MDXRemote } from 'next-mdx-remote/rsc';
3+
import JsonLd from '@/components/seo/JsonLd';
4+
import { createArticleSchema } from '@/lib/schema';
5+
import fs from 'fs';
6+
import path from 'path';
37

48
export async function generateStaticParams() {
59
return getAllStaticPaths('content/wiki');
610
}
711

812
export default async function WikiPage({ params }: any) {
9-
const { content, frontmatter } = getMDXContent('content/wiki', params.slug);
13+
const resolvedParams = await params;
14+
const slugPath = Array.isArray(resolvedParams.slug) ? resolvedParams.slug : [resolvedParams.slug];
15+
const { content, frontmatter } = getMDXContent('content/wiki', slugPath);
16+
const wikiUrl = `/wiki/${slugPath.join('/')}`;
17+
18+
// 파일 수정 시간 가져오기
19+
const wikiDirectory = path.join(process.cwd(), 'content/wiki');
20+
const filePath = path.join(wikiDirectory, ...slugPath) + '.mdx';
21+
let dateModified: string | undefined;
22+
let datePublished: string | undefined;
23+
24+
try {
25+
const stats = fs.statSync(filePath);
26+
dateModified = stats.mtime.toISOString();
27+
datePublished = stats.birthtime.toISOString();
28+
} catch (error) {
29+
// 파일이 없으면 현재 시간 사용
30+
dateModified = new Date().toISOString();
31+
datePublished = new Date().toISOString();
32+
}
33+
34+
const articleSchema = createArticleSchema(
35+
frontmatter.name || '패션 스타일 가이드',
36+
frontmatter.description || `${frontmatter.name}에 대한 상세한 패션 스타일 가이드입니다.`,
37+
frontmatter.image || '/TFT_icon.png',
38+
datePublished,
39+
dateModified,
40+
wikiUrl,
41+
'The First Take',
42+
'The First Take',
43+
'/TFT_icon.png'
44+
);
1045

1146
return (
12-
<article className="prose">
13-
<h1>{frontmatter.name}</h1>
14-
<MDXRemote source={content} components={{}} />
15-
</article>
47+
<>
48+
<JsonLd data={articleSchema} />
49+
<article className="prose">
50+
<h1>{frontmatter.name}</h1>
51+
<MDXRemote source={content} components={{}} />
52+
</article>
53+
</>
1654
);
1755
}

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,24 @@
7878
"ts-node": "^10.9.2",
7979
"typescript-eslint": "^8.44.1"
8080
},
81+
"browserslist": {
82+
"production": [
83+
">0.2%",
84+
"not dead",
85+
"not op_mini all",
86+
"not ie <= 11",
87+
"not chrome < 105",
88+
"not firefox < 100",
89+
"not safari < 14",
90+
"not edge < 105"
91+
],
92+
"development": [
93+
"last 1 chrome version",
94+
"last 1 firefox version",
95+
"last 1 safari version",
96+
"last 1 edge version"
97+
]
98+
},
8199
"pnpm": {
82100
"overrides": {
83101
"@eslint/plugin-kit": "0.3.4"

postcss.config.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const config = {
2-
plugins: ["@tailwindcss/postcss"],
2+
plugins: [
3+
"@tailwindcss/postcss",
4+
"autoprefixer",
5+
],
36
};
47

58
export default config;

src/components/seo/JsonLd.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* JSON-LD 구조화된 데이터를 렌더링하는 컴포넌트
3+
*/
4+
5+
import type {
6+
OrganizationSchema,
7+
WebSiteSchema,
8+
SoftwareApplicationSchema,
9+
ArticleSchema,
10+
WebPageSchema,
11+
} from '@/lib/schema';
12+
13+
type SchemaType =
14+
| OrganizationSchema
15+
| WebSiteSchema
16+
| SoftwareApplicationSchema
17+
| ArticleSchema
18+
| WebPageSchema
19+
| Record<string, unknown>;
20+
21+
interface JsonLdProps {
22+
data: SchemaType | SchemaType[];
23+
}
24+
25+
export default function JsonLd({ data }: JsonLdProps) {
26+
return (
27+
<script
28+
type="application/ld+json"
29+
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
30+
/>
31+
);
32+
}
33+

0 commit comments

Comments
 (0)