Skip to content

아키텍처

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는 서버에서 다음 작업을 수행합니다.

  1. generateMetadata: 초대 토큰으로 Loop 정보를 조회하여 OG 태그 생성 (제목, 설명, 이미지)
  2. 데이터 페칭: getLoopDetail, getMedia, getCommentsPromise.all로 병렬 호출
  3. 에러 처리: 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/requestSMS 인증번호 요청
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-hidden CSS로 초과 콘텐츠 숨김 (NoticeCard는 max-h-[100px])
  • ResizeObserverscrollHeight > 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인 경우 이 단계를 건너뜁니다.

단계 관리는 currentInputhighestReachedStep 상태로 처리하며, Framer Motion의 AnimatePresence로 슬라이드 전환 애니메이션을 적용합니다.

상태 관리

  • 폼 상태: useState로 관리 (React Hook Form 미사용)
  • 인증 상태: use-phone-verification.ts 커스텀 훅
    • Status flow: idlesendingsentverifyingverified
  • 비회원 데이터 캐싱: localStorageguest_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 인터랙션 컴포넌트 목록에 추가