สารบัญ (3 / 16)

03. 프론트엔드 (Next.js 16 · TypeScript · Tailwind CSS v4 · wagmi)

ขณะนี้เนื้อหาเอกสารมีเฉพาะภาษาเกาหลี

디렉터리 구조

frontend/
├─ src/
│  ├─ proxy.ts                    # 언어 접두사 리다이렉트 + 서버측 모바일 게이트 (/{locale}/dapp, 데스크톱 UA → mobile-only 403)
│  ├─ i18n/
│  │  ├─ config.ts                # 지원 언어, 이름, BCP47 태그, Accept-Language 매칭, localePath()
│  │  ├─ index.ts                 # Dictionary 타입(= typeof ko), getDictionary(locale), fmt()
│  │  ├─ client.tsx               # LocaleProvider, useDict(), useLocale()
│  │  └─ dictionaries/{ko,en,ja,zh,vi,fil,th}.ts   # 언어별 문구 (ko가 원본)
│  ├─ app/
│  │  ├─ globals.css              # Tailwind v4 진입점 + typography 플러그인 + 모바일 기본 스타일
│  │  └─ [locale]/
│  │     ├─ layout.tsx            # 루트 레이아웃: <html lang>, 메타데이터, LocaleProvider, generateStaticParams
│  │     ├─ page.tsx              # 공개 홈페이지 (사전에서 문구를 읽음)
│  │     ├─ docs/page.tsx         # 문서 목록 (../docs/*.md 를 빌드 시 읽음)
│  │     ├─ docs/[slug]/page.tsx  # 문서 본문 (react-markdown + remark-gfm, 정적 생성)
│  │     ├─ dapp/page.tsx         # <MobileGate><Providers><Dapp/></Providers></MobileGate>
│  │     ├─ mobile-only/page.tsx  # 데스크톱 안내 페이지
│  │     └─ not-found.tsx
│  ├─ components/
│  │  ├─ SiteHeader.tsx / SiteFooter.tsx / MobileNav.tsx  # 공용 헤더·푸터·햄버거 메뉴
│  │  ├─ LanguageSwitcher.tsx     # 언어 선택 (경로의 언어 세그먼트 교체 + 쿠키 저장)
│  │  ├─ Markdown.tsx             # 문서 렌더러 (NN-name.md 링크 → /{locale}/docs/NN-name 변환)
│  │  ├─ MobileGate.tsx           # 클라이언트측 모바일 게이트 (UA + pointer:coarse + touch)
│  │  ├─ MobileOnlyNotice.tsx     # 안내 문구
│  │  ├─ Providers.tsx            # WagmiProvider + React Query
│  │  ├─ Dapp.tsx                 # 화면 골격
│  │  ├─ ConnectWallet.tsx        # 연결/해제, 네트워크 전환, 지갑 딥링크
│  │  ├─ TokenBalance.tsx         # ACN 잔액 · BNB(가스) · USDT(BEP20 0x55d3…7955) 잔액
│  │  ├─ TransferForm.tsx         # 전송 폼 → writeContract → 영수증 대기 → 백엔드 기록
│  │  ├─ TransferHistory.tsx      # 백엔드 거래 내역 + 네트워크 동기화 상태
│  │  ├─ ReceiveCard.tsx          # 받기: 내 주소 QR(react-qr-code) · 주소 복사 · BscScan 링크
│  │  └─ DeckGallery.tsx          # 덱 슬라이드 갤러리 (라이트박스, 스와이프)
│  ├─ content/company.ts          # 언어 무관 상수 (회사명, 사이트, 배분 색상, 슬라이드 id)
│  └─ lib/
│     ├─ docs.ts                  # ../docs 마크다운 목록/읽기 (서버 전용, 빌드 시)
│     ├─ config.ts                # 토큰 주소, 체인, API URL, 익스플로러 링크
│     ├─ wagmi.ts                 # wagmi 설정 (injected + 선택적 walletConnect)
│     ├─ api.ts                   # 백엔드 REST 클라이언트
│     ├─ mobile.ts                # 모바일 판별 로직 (서버/클라이언트 공용)
│     ├─ useTokenMeta.ts          # symbol/decimals 공용 훅 (단일 소스)
│     └─ format.ts                # 금액/주소/시간 포맷
├─ public/brand/                  # 덱에서 추출한 슬라이드 WebP, 로고
├─ .env.example
└─ package.json

다국어 (i18n)

  • 지원 언어 7개: 한국어 ko(기본) · 영어 en · 일본어 ja · 중국어 번체 zh(대만·홍콩, zh-Hant) · 베트남어 vi · 필리핀어 fil · 태국어 th.
  • 모든 경로는 /{locale}/... 접두사를 가진다. 접두사 없는 요청은 proxy.tsNEXT_LOCALE 쿠키 → Accept-Language → 기본값 순으로 언어를 정해 307 리다이렉트한다. 중국어는 간체 브라우저도 번체로 안내한다.
  • 문구는 src/i18n/dictionaries/{ko,en,ja,zh,vi,fil,th}.ts에 있으며 ko.ts가 원본이다. 나머지 파일은 Dictionary 타입(= typeof ko)을 만족해야 하므로 키가 빠지면 빌드가 실패한다.
  • 서버 컴포넌트는 getDictionary(locale)로, 클라이언트 컴포넌트는 useDict()/useLocale()(LocaleProvider)로 문구를 읽는다. 자리표시자는 fmt("{chain} …", { chain }).
  • 헤더·모바일 메뉴·dApp 상단의 언어 선택기(LanguageSwitcher)는 현재 경로의 언어 세그먼트만 바꾸고 쿠키를 저장한다.
  • docs/*.md 본문은 한국어만 제공하며, 다른 언어에서는 안내 문구를 표시한다. 덱 슬라이드 이미지도 한국어 원본이다.
  • 각 언어는 generateStaticParams로 정적 생성된다(홈·문서·dApp·mobile-only × 7개 언어).

라우트

경로내용접근
/{locale}홈페이지: ALICE 플랫폼 소개(개요 · 시장 · 서비스 · 디바이스 · 토큰 · 글로벌 · 로드맵 · 파트너십 · 덱 갤러리). 콘텐츠 원본은 docs/10~15누구나, 모든 기기, 지갑 불필요
/{locale}/docs, /{locale}/docs/[slug]docs/*.md 를 정적으로 렌더링한 문서누구나, 모든 기기
/{locale}/dapp지갑 연결 · 잔액 · ACN 전송 · 내역 · 받기(QR)모바일 지갑 브라우저 전용
/{locale}/mobile-only데스크톱에서 /dapp 접근 시 안내-

모바일 전용 게이트 (2단계, /dapp 에만 적용)

단계위치판별결과
1src/proxy.ts (서버, /dapp 요청)User-AgentAndroid/iPhone/iPad/Mobile/TokenPocket/... 정규식에 매치되지 않으면 데스크톱/mobile-only로 rewrite, HTTP 403
2MobileGate.tsx (클라이언트, 마운트 시)UA 매치 또는 (pointer: coarse) && maxTouchPoints > 1데스크톱이면 <MobileOnlyNotice/>만 렌더
  • 2단계가 있는 이유: 1단계는 UA 위조로 통과될 수 있으므로 기기 능력(터치)으로 한 번 더 걸러낸다.
  • 의도된 제한: iPadOS Safari는 데스크톱 UA를 보내므로 1단계에서 차단된다. "절대 모바일 전용" 정책에 따라 데스크톱 UA를 보내는 태블릿은 지원하지 않는다. 지갑 앱 내장 브라우저(TokenPocket 등)는 모바일 UA를 보내므로 영향이 없다.
  • 판별 전에는 빈 화면을 렌더해 데스크톱에서 dApp UI가 깜빡이며 보이는 일이 없다.
  • 개발 중 PC에서 확인하려면 브라우저 DevTools의 기기 에뮬레이션(모바일 UA + 터치)을 켠다.

상태 관리

  • 체인 상태: wagmi 훅(useAccount, useReadContracts, useWriteContract, useWaitForTransactionReceipt).
  • 서버 상태: React Query(useQuery(["transfers", address])). 전송 성공 시 invalidateQueries로 갱신.
  • 토큰 메타데이터(symbol, decimals)는 lib/useTokenMeta.ts 한 곳에서 읽어 잔액·전송·내역이 공유한다. decimals를 읽기 전에는 전송 버튼이 비활성화되며 18로 가정하지 않는다.
  • 전송 결과는 영수증의 status === "success"로만 판정한다(영수증 도착 ≠ 성공). 지갑이 가속/교체한 트랜잭션은 영수증의 transactionHash를 백엔드에 기록한다. 백엔드는 RPC가 아직 tx를 못 보거나 확정 수(15블록, 약 45초)가 부족하면 404를 주므로, 지수 백오프로 최대 8회(수 분) 재시도한다.
  • useTokenMeta는 개별 호출 실패(status: "failure")도 오류로 승격해 "다시 시도" 버튼을 보여 준다.
  • 폼 상태: 컴포넌트 로컬 useState. 전역 스토어 없음.

스타일 · 모바일 최적화

  • Tailwind v4 (@import "tailwindcss"), 사이트 전체 다크 테마(<html class="dark"> + @custom-variant dark).
  • 내비게이션: md 미만에서는 햄버거 메뉴(MobileNav.tsx, 전체 화면 시트, 44px 터치 타깃), md 이상은 인라인 섹션 링크.
  • 표: 홈의 세대 비교표는 md 미만에서 카드로 전환. 문서의 마크다운 표·코드 블록은 가로 스크롤(.prose table).
  • 문서 목차: lg 미만은 접이식 <details>, lg 이상은 고정 사이드바.
  • 한국어 줄바꿈: word-break: keep-all + overflow-wrap: anywhere(주소·해시는 끊어짐).
  • iOS 입력 확대 방지: 뷰포트 maximumScale을 강제하지 않고(접근성) 모든 폼 컨트롤을 16px 이상으로 유지.
  • 터치 타깃 최소 44px, 히어로/CTA 버튼은 모바일에서 전체 폭, min-h-dvh, env(safe-area-inset-*).
  • 덱 갤러리 라이트박스: 좌우 스와이프, 열려 있는 동안 배경 스크롤 잠금.
  • dApp은 최대 폭 max-w-md로 큰 폰/폴더블에서도 한 손 조작 범위 유지.

환경 변수 (frontend/.env.local)

변수설명기본값
NEXT_PUBLIC_API_URLFastAPI 주소(운영: https://acn202610-api.onrender.com). 비우면 거래 내역 섹션이 숨겨지고 백엔드 호출을 하지 않는다빈 값
NEXT_PUBLIC_CHAIN_ID56(메인넷) / 97(테스트넷). 그 외 값은 빌드/기동 실패56
NEXT_PUBLIC_TOKEN_ADDRESSACN 컨트랙트. 주소 형식이 아니면 빌드/기동 실패0x84f9…6236
NEXT_PUBLIC_WC_PROJECT_IDWalletConnect Cloud 프로젝트 ID(비우면 비활성)빈 값

스크립트

npm run dev     # 개발 서버
npm run build   # 타입 체크 + 프로덕션 빌드
npm run lint    # ESLint