다크 모드
아키텍처
App Router 구조
Invite 서비스는 Next.js 16의 App Router를 사용하며, 동적 라우트 [token]을 중심으로 구성됩니다.
app/
├── layout.tsx # Server: 루트 레이아웃
├── page.tsx # Server: 루트 페이지
├── [token]/
│ ├── page.tsx # Server: 초대 메인 페이지
│ └── guest/page.tsx # 비회원 페이지 → [token]으로 리다이렉트
└── api/
├── proxy/[...path]/ # API 프록시 (CORS 우회)
└── branch/link/ # Branch 딥링크 생성초대 토큰([token])이 URL 경로의 핵심 파라미터이며, 이를 통해 서버에서 모임 데이터를 페칭합니다.
Server/Client Component 분리
Server Component
데이터 페칭과 SEO가 필요한 부분은 Server Component로 구현합니다.
app/layout.tsx → 폰트 로드, NextIntlClientProvider 설정
app/[token]/page.tsx → API 호출 (getLoopDetail, getMedia, getComments)
OG 메타데이터 생성 (generateMetadata)app/[token]/page.tsx는 서버에서 다음 작업을 수행합니다.
generateMetadata: 초대 토큰으로 Loop 정보를 조회하여 OG 태그 생성 (제목, 설명, 이미지)- 데이터 페칭:
getLoopDetail,getMedia,getComments를Promise.all로 병렬 호출 - 에러 처리: API 호출 실패 시
ErrorCard컴포넌트 렌더링
Client Component
사용자 인터랙션이 필요한 컴포넌트는 'use client' 지시자를 사용합니다.
guest-registration-card.tsx— 비회원 등록 카드 (Loop 상태 분기, 폼 래핑)guest-registration-form.tsx— 비회원 등록 멀티스텝 폼 (이름 → 전화번호 → 인증 → 날짜 선택)verification-step.tsx— SMS OTP 인증tab-navigation.tsx— 탭 전환top-banner.tsx— 앱 다운로드 배너 (스크롤 감지)description-section.tsx— 룹 설명 렌더링 (마크다운/HTML 이중 전략, XSS sanitization, 높이 트렁케이션 + More/Less 토글)photos-gallery.tsx— 이미지 갤러리 인터랙션
API 통신
이중 환경 분기
API 호출은 실행 환경에 따라 자동으로 경로가 분기됩니다.
- 서버 측:
process.env.NEXT_PUBLIC_API_BASE_URL로 직접 호출 (CORS 제한 없음) - 브라우저 측:
/invite/api/proxy/...프록시 경로를 통해 호출 (CORS 우회)
API 프록시
app/api/proxy/[...path]/route.ts는 catch-all 라우트로, 브라우저의 요청을 https://api.truloop.app/core/api/로 전달합니다. GET과 POST 메서드를 지원합니다.
주요 API 엔드포인트
| 메서드 | 경로 | 용도 |
|---|---|---|
| GET | /public/v1/invitations/{token} | 초대 상세 정보 조회 |
| GET | /public/v1/invitations/{token}/media | 미디어 목록 조회 |
| GET | /public/v1/invitations/{token}/comments | 댓글 목록 조회 |
| POST | /public/v1/invitations/{token}/check-phone | 전화번호 회원 여부 확인 |
| POST | /public/v1/invitations/{token}/guest | 비회원 참여 등록 |
| POST | /v1/phone-verification/request | SMS 인증번호 요청 |
| POST | /v1/phone-verification/verify | 인증번호 검증 |
컴포넌트 설계
shadcn/ui 기반 UI 시스템
UI 컴포넌트는 shadcn/ui의 base-vega 스타일을 기반으로 구성됩니다. components.json 설정에 따라 @base-ui/react 프리미티브 위에 Tailwind CSS로 스타일링합니다.
typescript
// lib/utils.ts - className 조합 유틸리티
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}CVA (Class Variance Authority) 패턴
컴포넌트의 variant를 정의할 때 CVA를 사용합니다.
typescript
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva("inline-flex items-center justify-center...", {
variants: {
variant: {
default: "bg-primary text-primary-foreground...",
outline: "border border-input bg-background...",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
},
},
});OKLch 색상 체계
globals.css에서 OKLch 색상 공간을 사용하는 CSS 변수를 정의합니다. Tailwind CSS v4의 색상 시스템과 연동됩니다.
콘텐츠 렌더링
이중 렌더링 전략 (DescriptionSection)
룹 설명 텍스트의 콘텐츠 유형을 자동 감지하여 렌더링 경로를 분기합니다:
- HTML 감지:
/<[a-z][\s\S]*>/i정규식으로 HTML 태그 포함 여부 판별 - HTML 경로:
DOMPurify.sanitize()로 허용 태그/속성 화이트리스트 적용 후dangerouslySetInnerHTML로 렌더링- 허용 태그: h1-h6, p, br, strong, em, b, i, u, s, a, ul, ol, li, blockquote, code, pre, hr, img, span, div, sub, sup
- 허용 속성: href, target, rel, src, alt, class, style
- 마크다운 경로:
react-markdown기반MarkdownViewer컴포넌트로 렌더링 (h1-h3, p, a, ul/ol/li, blockquote, code, pre, hr, img, strong 지원) - 메모이제이션:
useMemo로 sanitization 결과 캐싱
높이 트렁케이션
긴 설명 텍스트의 접기/펼치기를 처리합니다:
max-h-[120px] overflow-hiddenCSS로 초과 콘텐츠 숨김 (NoticeCard는max-h-[100px])ResizeObserver로scrollHeight > clientHeight + 1비교하여 토글 필요 여부 동적 감지- description prop 변경 시
isExpanded/canToggle상태 자동 초기화 - i18n 키:
invite.notice.more(더보기),invite.notice.less(접기)
멀티스텝 폼 아키텍처
비회원 등록 폼은 4단계로 구성됩니다.
이름 입력 (name)
사용자 이름을 입력합니다.
전화번호 입력 (phone)
국가 코드와 전화번호를 입력합니다. libphonenumber-js로 유효성 검증 후, 기존 회원 여부를 API로 확인합니다.
전화번호 인증 (verification)
SMS OTP 코드를 입력하여 본인 인증을 완료합니다. 60초 재전송 쿨다운과 토큰 만료 관리를 포함합니다.
날짜 선택 (dates)
react-day-picker 캘린더에서 참석 가능한 날짜를 선택합니다. Loop 상태가 CONFIRMED인 경우 이 단계를 건너뜁니다.
단계 관리는 currentInput과 highestReachedStep 상태로 처리하며, Framer Motion의 AnimatePresence로 슬라이드 전환 애니메이션을 적용합니다.
상태 관리
- 폼 상태:
useState로 관리 (React Hook Form 미사용) - 인증 상태:
use-phone-verification.ts커스텀 훅- Status flow:
idle→sending→sent→verifying→verified
- Status flow:
- 비회원 데이터 캐싱:
localStorage에guest_registration_{token}키로 저장 (lib/guest-storage.ts)
에러 처리
API 에러를 i18n 키로 변환하여 다국어 에러 메시지를 표시합니다.
typescript
// hooks/use-phone-verification.ts
const ERROR_TYPE_TO_I18N: Record<string, string> = {
INVALID_VERIFICATION_CODE: 'invalidCode',
ERROR_TYPE_VERIFICATION_EXPIRED: 'codeExpired',
ERROR_TYPE_NOT_FOUND: 'codeExpired',
TOO_MANY_VERIFICATION_ATTEMPTS: 'tooManyAttempts',
UNSUPPORTED_COUNTRY: 'unsupportedCountry',
INVALID_PHONE_NUMBER: 'generic',
};반응형 레이아웃
초대 페이지는 모바일 우선으로 설계되며, 데스크톱에서는 2컬럼 레이아웃을 사용합니다.
- 모바일: 세로 스크롤, 커버 이미지가 상단에 위치
- 데스크톱 (
md:이상): 좌측 2/5에 커버 이미지 (sticky), 우측 3/5에 스크롤 가능한 콘텐츠
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-16 | 콘텐츠 렌더링 섹션 추가 (이중 렌더링 전략, XSS sanitization, 높이 트렁케이션) |
| 2026-03-10 | 소스 코드 검증: ERROR_TYPE_TO_I18N 매핑 실제 코드와 동기화, GuestRegistrationCard 인터랙션 컴포넌트 목록에 추가 |