diff --git a/mobile/README.md b/mobile/README.md
index 87b0eca..64fbe71 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -7,6 +7,19 @@ React Native + Expo(expo-router, TypeScript) 모바일 앱. 조회·승인·현
- Expo SDK 51 · expo-router ~3.5 · React Native 0.74 · TypeScript
- JWT는 `expo-secure-store`(네이티브 keychain/keystore)에 저장, 웹은 AsyncStorage 폴백
- 디자인 토큰: `theme/`(docs/design.md §1 팔레트·타이포). 신규 토큰 없음.
+- 다국어: `react-i18next` + `expo-localization`(SDK51 호환). 지원 ko(기본/폴백)·en·zh·ja.
+
+## 다국어(i18n)
+- 초기화 `lib/i18n/`(동기 init — 디바이스 로케일 감지, 저장 선택은 부팅 시 반영). 리소스 `lib/i18n/locales/{ko,en,zh,ja}.ts`.
+- **ko가 키 형상의 단일 출처** — en/zh/ja는 `Translations`(DeepString) 타입 구현이라 키 누락 시 tsc가 잡음(병렬 편집 함정 회피: 직렬·키 대조).
+- 언어 선택 지속: AsyncStorage 키 `kintex_lang`. 전환 UI `components/LanguageSelector.tsx`(더보기·내정보에 배치), 상태 `context/LanguageContext.tsx`.
+- 커버 화면(우선순위): 로그인/2FA·회원가입·비번찾기·홈·현장·갤러리·더보기·내정보·티켓 지갑·탭/스택 타이틀·역할 라벨. 미커버(폴백=현행 한국어): `checklist`·`inspection`·`tickets/select`·티켓 카드/필터탭 컴포넌트(`components/tickets/*`).
+
+## 역할별 랜딩
+- `lib/roleTrack.ts` — 웹 `src/frontend/src/lib/roleTrack.ts` 패리티(우선순위 admin>ops>business>agency>visitor, 미판정=visitor 기본).
+- 로그인 응답 user에 roleCode가 없어 `hallManager`를 ops 신호로 사용(roleCode 아는 호출부는 옵션 전달).
+- 모바일 랜딩 매핑: admin/ops/business→`/(tabs)` 홈 · agency(장치·시공)→`/(tabs)/field` · visitor(관람객)→`/tickets`.
+- 콜드 부팅 시 workspaces 미복원 misroute 방지 위해 로그인 시 계산한 랜딩 경로를 secure-store에 지속(`AuthContext`).
## 실행
```bash
@@ -48,5 +61,6 @@ API 베이스는 `app.json > extra.apiBase` 또는 `EXPO_PUBLIC_API_BASE` 환경
## 남은 작업
- 실 화면 데이터 API 배선 확대(부스 목록·검수 요청 제출 — 백엔드 M6/C-4 확장 대기).
-- 앱 아이콘/스플래시 에셋(`assets/`), i18n, 푸시 알림.
-- EAS 실빌드(eas.json 프로파일) — 본 스캐폴드 범위 밖.
+- 앱 아이콘/스플래시 에셋(`assets/`), 푸시 알림.
+- i18n 잔여 화면 키 추출: `checklist`·`inspection`·`tickets/select`·`components/tickets/*`(현재 한국어 폴백).
+- EAS 실빌드(eas.json 프로파일) — G3 게이트(소유자 승인).
diff --git a/mobile/app.json b/mobile/app.json
index c4032f3..21dd44e 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -4,7 +4,7 @@
"slug": "kintex",
"owner": "zioinfo",
"scheme": "kintex",
- "version": "0.1.0",
+ "version": "0.2.0",
"orientation": "portrait",
"userInterfaceStyle": "light",
"jsEngine": "hermes",
@@ -28,7 +28,7 @@
},
"android": {
"package": "kr.co.zioinfo.kintex",
- "versionCode": 1,
+ "versionCode": 2,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0066B3"
@@ -58,7 +58,6 @@
"expo-router",
"expo-secure-store",
"expo-font",
- "expo-screen-capture",
[
"expo-image-picker",
{
diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx
index fccc948..f364588 100644
--- a/mobile/app/(tabs)/_layout.tsx
+++ b/mobile/app/(tabs)/_layout.tsx
@@ -5,10 +5,12 @@
import { Ionicons } from '@expo/vector-icons';
import { Redirect, Tabs } from 'expo-router';
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { colors } from '../../theme';
export default function TabsLayout() {
+ const { t } = useTranslation();
const { ready, token } = useAuth();
if (ready && !token) return ;
@@ -25,14 +27,14 @@ export default function TabsLayout() {
,
}}
/>
(
),
@@ -41,14 +43,14 @@ export default function TabsLayout() {
,
}}
/>
(
),
diff --git a/mobile/app/(tabs)/field.tsx b/mobile/app/(tabs)/field.tsx
index b3c1912..0f6be33 100644
--- a/mobile/app/(tabs)/field.tsx
+++ b/mobile/app/(tabs)/field.tsx
@@ -5,12 +5,14 @@
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Banner } from '../../components/Banner';
import { useAuth } from '../../context/AuthContext';
import { colors, radius, spacing, type } from '../../theme';
export default function FieldScreen() {
+ const { t } = useTranslation();
const { activeWorkspace, user } = useAuth();
const role = activeWorkspace?.myRole;
const isHallManager = user?.hallManager || role === 'HALL_MANAGER';
@@ -18,19 +20,17 @@ export default function FieldScreen() {
return (
- 현장 업무
+ {t('field.title')}
{!isContractor && !isHallManager ? (
-
- 현장 기능은 장치·시공업체(체크리스트)와 홀매니저(검수) 역할에서 제공됩니다.
-
+ {t('field.guide')}
) : null}
{isContractor ? (
router.push('/checklist')}
/>
) : null}
@@ -38,8 +38,8 @@ export default function FieldScreen() {
{isHallManager ? (
router.push('/inspection')}
/>
) : null}
diff --git a/mobile/app/(tabs)/gallery.tsx b/mobile/app/(tabs)/gallery.tsx
index 3826b97..03bc473 100644
--- a/mobile/app/(tabs)/gallery.tsx
+++ b/mobile/app/(tabs)/gallery.tsx
@@ -4,7 +4,8 @@
* 실 API 호출 구조를 유지하되, 서버 미구현/오프라인 시 degraded 샘플로 폴백한다.
*/
import { router } from 'expo-router';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { AiImage } from '../../components/AiImage';
import { Banner } from '../../components/Banner';
@@ -18,14 +19,21 @@ import { colors, spacing, type } from '../../theme';
const DEMO_BOOTH = 'b-102';
// degraded 폴백 샘플 (서버 미구현/오프라인 시 표시 — 항상 워터마크 유지)
-const FALLBACK: RenderJobDto[] = [
- mkJob('job-s1', 'S1', 'DONE'),
- mkJob('job-s2', 'S2', 'DONE'),
- mkJob('job-s3', 'S3', 'RUNNING'),
- mkJob('job-s7', 'S7', 'FAILED'),
-];
+function buildFallback(notice: string): RenderJobDto[] {
+ return [
+ mkJob('job-s1', 'S1', 'DONE', notice),
+ mkJob('job-s2', 'S2', 'DONE', notice),
+ mkJob('job-s3', 'S3', 'RUNNING', notice),
+ mkJob('job-s7', 'S7', 'FAILED', notice),
+ ];
+}
-function mkJob(jobId: string, shot: string, status: RenderJobDto['status']): RenderJobDto {
+function mkJob(
+ jobId: string,
+ shot: string,
+ status: RenderJobDto['status'],
+ notice: string,
+): RenderJobDto {
return {
jobId,
boothId: DEMO_BOOTH,
@@ -36,7 +44,7 @@ function mkJob(jobId: string, shot: string, status: RenderJobDto['status']): Ren
modelVersion: null,
watermarkRequired: true,
watermarkText: AI_IMAGE_NOTICE,
- notice: 'AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 (시공 기준은 도면)',
+ notice,
};
}
@@ -51,15 +59,18 @@ const SHOT_LABEL: Record = {
};
export default function GalleryScreen() {
+ const { t } = useTranslation();
const { activeWorkspace, token } = useAuth();
const [jobs, setJobs] = useState([]);
const [degraded, setDegraded] = useState(false);
const [loading, setLoading] = useState(true);
+ const fallback = useMemo(() => buildFallback(t('gallery.notice')), [t]);
+
const load = useCallback(async () => {
setLoading(true);
if (!activeWorkspace || !token) {
- setJobs(FALLBACK);
+ setJobs(fallback);
setDegraded(true);
setLoading(false);
return;
@@ -72,7 +83,7 @@ export default function GalleryScreen() {
setDegraded(false);
} catch (e) {
if (isDegraded(e)) {
- setJobs(FALLBACK);
+ setJobs(fallback);
setDegraded(true);
} else {
setJobs([]);
@@ -81,7 +92,7 @@ export default function GalleryScreen() {
} finally {
setLoading(false);
}
- }, [activeWorkspace, token]);
+ }, [activeWorkspace, token, fallback]);
useEffect(() => {
load();
@@ -91,9 +102,7 @@ export default function GalleryScreen() {
{/* 상단 고정 고지 바 (제거 불가) */}
-
- AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다
-
+ {t('gallery.infoBar')}
}
>
{degraded ? (
-
- 서버 이미지 이력에 연결할 수 없어 예시를 표시합니다 (degraded).
-
+ {t('gallery.degraded')}
) : null}
{jobs.length === 0 && !loading ? (
-
- 아직 생성된 이미지가 없습니다 — 설계 스튜디오에서 예상 사진을 생성하세요.
-
+ {t('gallery.empty')}
) : null}
@@ -124,14 +129,14 @@ export default function GalleryScreen() {
height={130}
/>
{j.status === 'FAILED' ? (
-
+
) : null}
))}