The document text is currently available in Korean only.
디렉터리 구조
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.ts가 NEXT_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 에만 적용)
| 단계 | 위치 | 판별 | 결과 |
|---|
| 1 | src/proxy.ts (서버, /dapp 요청) | User-Agent가 Android/iPhone/iPad/Mobile/TokenPocket/... 정규식에 매치되지 않으면 데스크톱 | /mobile-only로 rewrite, HTTP 403 |
| 2 | MobileGate.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_URL | FastAPI 주소(운영: https://acn202610-api.onrender.com). 비우면 거래 내역 섹션이 숨겨지고 백엔드 호출을 하지 않는다 | 빈 값 |
NEXT_PUBLIC_CHAIN_ID | 56(메인넷) / 97(테스트넷). 그 외 값은 빌드/기동 실패 | 56 |
NEXT_PUBLIC_TOKEN_ADDRESS | ACN 컨트랙트. 주소 형식이 아니면 빌드/기동 실패 | 0x84f9…6236 |
NEXT_PUBLIC_WC_PROJECT_ID | WalletConnect Cloud 프로젝트 ID(비우면 비활성) | 빈 값 |
스크립트
npm run dev # 개발 서버
npm run build # 타입 체크 + 프로덕션 빌드
npm run lint # ESLint