diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f60c750
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,91 @@
+# Mingling 프론트엔드 작업 지침
+
+## 1. 적용 범위와 우선순위
+
+- 이 지침은 저장소 전체에 적용한다.
+- 현재 UI와 서비스 기능을 기본적으로 보존한다.
+- 사용자가 UI 또는 기능 변경을 명시적으로 요청한 경우에만 해당 요청 범위 내에서 예외를 허용한다.
+- 사용자 승인 없이 하위 디렉터리에 `AGENTS.md` 또는 `AGENTS.override.md`를 생성하지 않는다.
+- 작업을 완료하려면 이 지침을 위반해야 한다고 판단되는 경우, 파일을 수정하지 말고 충돌 내용과 필요한 결정을 사용자에게 설명한다.
+
+## 2. UI 불변 조건
+
+- 사용자가 명시적으로 요청하지 않은 문구, 라벨, 아이콘, 색상, 폰트, 글자 크기, 굵기, 줄 높이, 자간을 변경하지 않는다.
+- 여백, 크기, 정렬, 테두리, 그림자, 애니메이션, 반응형 분기점, 이미지 크롭, 페이지 구성을 변경하지 않는다.
+- 화면에 보이는 요소를 임의로 추가, 제거, 재배치하거나 스타일을 변경하지 않는다.
+- 375px, 768px, 1024px, 1440px 뷰포트에서 현재 화면의 시각적 결과를 보존한다.
+- 최종 렌더링 결과가 시각적으로 동일한 경우에만 내부 DOM, 컴포넌트 구조, 이미지 렌더링 방식, 리소스 로딩 방식을 변경할 수 있다.
+- 사용자 승인 없이 폰트 패밀리, 등록된 폰트 굵기, 타이포그래피 메트릭, 폰트 preload 정책을 변경하지 않는다.
+- 이미지, 지도, 다이얼로그, 로딩 화면, 동적 콘텐츠의 크기와 종횡비를 유지하고 새로운 레이아웃 이동을 발생시키지 않는다.
+
+## 3. 기능 불변 조건
+
+- 사용자가 요청하지 않은 기능을 추가, 제거하거나 의미를 변경하지 않는다.
+- 라우트, 리다이렉트, 링크 목적지, 쿼리 파라미터를 변경하지 않는다.
+- API 엔드포인트, 요청 및 응답 구조, 환경 변수 이름을 변경하지 않는다.
+- localStorage 및 sessionStorage 키, 로그인 판정과 인증 동작을 변경하지 않는다.
+- 폼 검증, 제출 순서, 오류 처리, 로딩 상태, 모달 동작을 유지한다.
+- 지도, 장소 추천, 모임 생성·참여·공유 흐름과 반응형 상호작용을 유지한다.
+- 분석 이벤트 이름, payload, 전송 순서와 GTM 로드 전 `dataLayer` 큐 동작을 유지한다.
+- 성능 최적화로 인해 해당 화면의 필수 기능이 늦게 활성화되거나 비활성화되어서는 안 된다.
+- 사용자가 명시적으로 요청하지 않은 공개 TypeScript 인터페이스나 공용 데이터 계약을 변경하지 않는다.
+
+## 4. 변경 범위 관리
+
+- 파일을 수정하기 전에 `git status`와 관련 파일을 확인한다.
+- 사용자가 이미 작성한 변경 사항을 보존하며, 관련 없는 변경을 되돌리거나 덮어쓰지 않는다.
+- 요청을 충족하는 데 필요한 최소한의 파일과 코드만 수정한다.
+- 관련 없는 코드 정리, 대규모 리팩터링, 파일 이동, 이름 변경, 저장소 전체 포매팅을 수행하지 않는다.
+- 자동 생성 파일을 직접 수정하지 않는다.
+- 저장소의 lockfile과 CI 설정에 맞춰 `pnpm`을 사용한다.
+- 새로운 production 의존성을 추가하려면 사용자 승인을 먼저 받는다.
+- 사용되지 않는 것으로 확인된 의존성은 제거할 수 있으며, `package.json`과 `pnpm-lock.yaml`을 함께 갱신한다.
+- 사용자가 요청하지 않은 패키지 버전 업그레이드는 수행하지 않는다.
+- `.env*` 파일의 비밀값을 출력하거나 코드와 문서에 포함하거나 커밋하지 않는다.
+
+## 5. 성능 최적화 제약조건
+
+- 화면을 변경하지 않는 범위에서 리소스 발견 시점, 우선순위, 이미지 포맷, 캐시, 번들 경계, 스크립트 실행 시점을 최적화한다.
+- Above the Fold 리소스 중 실제 LCP 후보로 확인된 항목만 높은 우선순위를 부여한다.
+- Below the Fold 이미지는 지연 로딩을 유지한다.
+- CSS로 숨겨진 반응형 이미지까지 preload하거나 eager loading하지 않는다.
+- 반응형 이미지 preload의 `srcset`, `sizes`, media query와 실제 이미지 요청 URL이 일치해야 한다.
+- 모든 페이지에서 사용하지 않는 리소스 힌트는 해당 라우트에만 적용한다.
+- 서드파티 스크립트는 기존 기능과 이벤트 큐가 보존되는 경우에만 지연시킨다.
+- 최적화 전 기준값을 기록하고 동일한 라우트, 뷰포트, 네트워크 제한, 캐시 상태에서 결과를 비교한다.
+- 성능 목표를 달성하려면 UI 또는 기능 불변 조건을 위반해야 하는 경우, 제약조건을 임의로 완화하지 말고 남은 병목을 사용자에게 보고한다.
+
+## 6. 필수 검증
+
+런타임 코드, 렌더링, 리소스 또는 의존성을 변경한 경우 다음 명령을 실행한다.
+
+1. `pnpm exec tsc --noEmit`
+2. `pnpm run lint`
+3. `pnpm run build`
+
+다음 항목도 확인한다.
+
+- `pnpm run start`로 로컬 프로덕션 서버를 실행하고 영향받은 흐름을 테스트한다.
+- 새로운 브라우저 콘솔 오류, 요청 실패, hydration 경고, 접근성 회귀가 없어야 한다.
+- 375px, 768px, 1024px, 1440px에서 변경 전후 화면을 비교한다.
+- 문구, 레이아웃, 이미지 크롭, 버튼, 폼, 모달, 지도와 반응형 동작이 동일해야 한다.
+- Network 패널에서 preload, 요청 우선순위, 이미지 포맷, 서드파티 로딩 시점이 의도대로 동작하는지 확인한다.
+
+랜딩 페이지 성능 작업은 cold cache와 Lighthouse Mobile 기본 환경에서 3회 측정하고 중앙값을 기록한다.
+
+- Performance 점수: 90점 이상
+- LCP: 1.5초 이하
+- TBT: 50ms 이하
+- CLS: 0.01 이하
+- FCP: 기록된 기준값에서 유의미하게 악화되지 않을 것
+
+필수 검증이 실패한 상태에서는 작업을 완료한 것으로 간주하지 않는다. 범위 안에서 실패를 해결하거나 정확한 차단 원인을 보고한다.
+
+## 7. 작업 완료 보고
+
+- 변경한 모든 파일을 나열한다.
+- 주요 변경 사항의 Before/After 차이를 설명한다.
+- 실행한 검증 명령과 결과를 보고한다.
+- 수행한 화면 및 기능 검증 내용을 보고한다.
+- 성능 작업은 Lighthouse와 네트워크 측정의 변경 전후 결과를 제공한다.
+- 실행하지 못한 검증, 남아 있는 위험과 완료를 막은 제약조건을 명시한다.
diff --git a/app/globals.css b/app/globals.css
index 09468ea..66f7e61 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,6 +1,15 @@
@import 'tailwindcss';
@import 'tw-animate-css';
+@font-face {
+ font-family: 'Pretendard Fallback';
+ src: local('Arial');
+ ascent-override: 93.76%;
+ descent-override: 23.75%;
+ line-gap-override: 0%;
+ size-adjust: 101.55%;
+}
+
/* 사용자 정의 컬러 시스템 */
@theme {
--color-gray-1: #f2f3f8;
@@ -59,6 +68,7 @@
}
:root {
+ --font-pretendard: 'Pretendard', 'Pretendard Fallback';
--radius: 0.625rem;
/* 흰색 배경, 검은색 글씨 */
diff --git a/app/layout.tsx b/app/layout.tsx
index da93d99..f0ebfe8 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,28 +1,12 @@
import './globals.css';
import type { Metadata } from 'next';
import Script from 'next/script';
-import localFont from 'next/font/local';
import Header from '../components/header';
import Footer from '../components/footer';
import GlobalModal from '@/components/modal/globalModal';
import QueryProvider from '@/components/providers/queryProvider';
-import { GoogleTagManager } from '@next/third-parties/google';
-const pretendard = localFont({
- src: [
- { path: '../public/fonts/Pretendard-Thin.woff2', weight: '100' },
- { path: '../public/fonts/Pretendard-ExtraLight.woff2', weight: '200' },
- { path: '../public/fonts/Pretendard-Light.woff2', weight: '300' },
- { path: '../public/fonts/Pretendard-Regular.woff2', weight: '400' },
- { path: '../public/fonts/Pretendard-Medium.woff2', weight: '500' },
- { path: '../public/fonts/Pretendard-SemiBold.woff2', weight: '600' },
- { path: '../public/fonts/Pretendard-Bold.woff2', weight: '700' },
- { path: '../public/fonts/Pretendard-ExtraBold.woff2', weight: '800' },
- { path: '../public/fonts/Pretendard-Black.woff2', weight: '900' },
- ],
- variable: '--font-pretendard',
- display: 'swap',
-});
+const GTM_ID = 'GTM-MSQ45TJD';
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || 'https://www.mingling.kr'),
@@ -37,8 +21,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
+