Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
EXPO_PUBLIC_BASE_URL=https://api.example.com
EXPO_PUBLIC_WEBVIEW_URL=https://www.example.com
EXPO_PUBLIC_MIXPANEL_TOKEN=
82 changes: 82 additions & 0 deletions .github/workflows/android-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Android Check

on:
pull_request:
push:
branches:
- main

jobs:
android-check:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Create public env file
run: |
{
echo "EXPO_PUBLIC_BASE_URL=${EXPO_PUBLIC_BASE_URL:-https://api.example.com}"
echo "EXPO_PUBLIC_WEBVIEW_URL=${EXPO_PUBLIC_WEBVIEW_URL:-https://www.example.com}"
echo "EXPO_PUBLIC_MIXPANEL_TOKEN=${EXPO_PUBLIC_MIXPANEL_TOKEN:-}"
} > .env
env:
EXPO_PUBLIC_BASE_URL: ${{ secrets.EXPO_PUBLIC_BASE_URL || vars.EXPO_PUBLIC_BASE_URL }}
EXPO_PUBLIC_WEBVIEW_URL: ${{ secrets.EXPO_PUBLIC_WEBVIEW_URL || vars.EXPO_PUBLIC_WEBVIEW_URL }}
EXPO_PUBLIC_MIXPANEL_TOKEN: ${{ secrets.EXPO_PUBLIC_MIXPANEL_TOKEN || vars.EXPO_PUBLIC_MIXPANEL_TOKEN }}

- name: Run lint
run: npm run lint

- name: Create placeholder google-services.json
run: |
cat > google-services.json <<'JSON'
{
"project_info": {
"project_number": "123456789012",
"project_id": "moadong-ci",
"storage_bucket": "moadong-ci.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789012:android:0000000000000000000000",
"android_client_info": {
"package_name": "com.moadong.moadong"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "DUMMY_API_KEY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
JSON

- name: Prebuild Android
run: npx expo prebuild --platform android --clean
env:
CI: "1"

- name: Assemble debug
working-directory: android
run: ./gradlew :app:assembleDebug
89 changes: 89 additions & 0 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Android Release

on:
push:
branches:
- prod
workflow_dispatch:
inputs:
lane:
description: Fastlane Android lane to run
required: true
type: choice
default: internal
options:
- internal
- production_draft

jobs:
android-release:
runs-on: ubuntu-latest

env:
ANDROID_PACKAGE_NAME: com.moadong.moadong
EXPO_PUBLIC_BASE_URL: ${{ secrets.EXPO_PUBLIC_BASE_URL || vars.EXPO_PUBLIC_BASE_URL }}
EXPO_PUBLIC_WEBVIEW_URL: ${{ secrets.EXPO_PUBLIC_WEBVIEW_URL || vars.EXPO_PUBLIC_WEBVIEW_URL }}
EXPO_PUBLIC_MIXPANEL_TOKEN: ${{ secrets.EXPO_PUBLIC_MIXPANEL_TOKEN || vars.EXPO_PUBLIC_MIXPANEL_TOKEN }}
SUPPLY_JSON_KEY: ${{ github.workspace }}/google-play-service-account.json
MYAPP_UPLOAD_STORE_FILE: ${{ secrets.MYAPP_UPLOAD_STORE_FILE || 'moadong-upload.keystore' }}
MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }}
MYAPP_UPLOAD_KEY_ALIAS: ${{ secrets.MYAPP_UPLOAD_KEY_ALIAS }}
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }}
FASTLANE_ANDROID_LANE: ${{ github.event_name == 'push' && 'production_draft' || github.event.inputs.lane || 'internal' }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true

- name: Install Node dependencies
run: npm ci

- name: Validate Android release secrets
run: |
test -n "$ANDROID_KEYSTORE_BASE64"
test -n "$MYAPP_UPLOAD_STORE_FILE"
test -n "$MYAPP_UPLOAD_STORE_PASSWORD"
test -n "$MYAPP_UPLOAD_KEY_ALIAS"
test -n "$MYAPP_UPLOAD_KEY_PASSWORD"
test -n "$GOOGLE_SERVICES_JSON_BASE64"
test -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"
test -n "$EXPO_PUBLIC_BASE_URL"
test -n "$EXPO_PUBLIC_WEBVIEW_URL"
test -n "$EXPO_PUBLIC_MIXPANEL_TOKEN"
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}

- name: Create public env file
run: |
{
echo "EXPO_PUBLIC_BASE_URL=${EXPO_PUBLIC_BASE_URL}"
echo "EXPO_PUBLIC_WEBVIEW_URL=${EXPO_PUBLIC_WEBVIEW_URL}"
echo "EXPO_PUBLIC_MIXPANEL_TOKEN=${EXPO_PUBLIC_MIXPANEL_TOKEN}"
} > .env

- name: Restore Android secret files
run: |
printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > google-services.json
printf '%s' "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > "$SUPPLY_JSON_KEY"
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$MYAPP_UPLOAD_STORE_FILE"
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}

- name: Upload Android release
run: bundle exec fastlane android "$FASTLANE_ANDROID_LANE"
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ yarn-error.*
app-example

# generated native folders
/ios
/android

/.env
google-services.json
GoogleService-Info.plist
GoogleService-Info.plist
ios/**/GoogleService-Info.plist
90 changes: 90 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# AGENTS.md

이 파일은 Codex (Codex.ai/code)가 이 저장소에서 작업할 때 참고하는 가이드입니다.

## 프로젝트 개요

**모아동 (Moadong)** — 대학교 동아리 탐색 및 알림 구독을 위한 React Native + Expo 앱. 사용자는 동아리를 탐색하고 푸시 알림을 구독하며, WebView를 통해 동아리 상세 페이지를 볼 수 있습니다.

- **Bundle ID**: `com.moadong.moadong`
- **딥링크 스킴**: `moadongapp://`, associated domain: `www.moadong.com`
- **React Native New Architecture** 활성화 (`newArchEnabled: true`)
- **React Compiler** (실험적 기능) 활성화

## 명령어

```bash
npm start # Expo 개발 서버 시작
npm run ios # iOS 시뮬레이터 실행
npm run android # Android 에뮬레이터 실행
npm run lint # ESLint 실행 (expo lint)
npx expo start --dev-client # 개발 클라이언트 빌드로 시작
```

환경 변수: API 기본 URL은 `.env`에 `EXPO_PUBLIC_BASE_URL`로 설정합니다.

## 아키텍처

### 라우팅 (Expo Router 파일 기반)
```text
app/
_layout.tsx # 루트 레이아웃: 부트스트랩, 스플래시, 강제 업데이트, Context 프로바이더
(tabs)/ # 하단 탭 네비게이터
index.tsx # 홈 탭
more.tsx # 더보기 탭
club/[id].tsx # 동아리 상세 (WebView)
clubDetail/[id].tsx # 동아리 상세 (네이티브)
webview/[slug].tsx # 범용 WebView 화면
modal.tsx # 모달 화면
```

### 부트스트랩 순서 (app/_layout.tsx)
앱 시작 시 루트 레이아웃이 다음 순서로 실행됩니다:
1. Firebase Remote Config를 통한 강제 업데이트 체크
2. iOS ATT (앱 추적 투명성) 권한 요청
3. 액세스 토큰 조회/생성 (`auth-token-storage`)
4. FCM 토큰 등록
5. 서버에서 구독 동아리 목록 동기화
6. Mixpanel 애널리틱스 초기화

부트스트랩이 완료될 때까지 커스텀 스플래시 화면이 UI를 차단합니다.

### API 레이어 (services/api.ts)
두 가지 Axios 클라이언트 인스턴스:
- `publicApi` — 인증 없는 요청
- `authApi` — `Bearer` 토큰 자동 첨부; 401 응답 시 `/auth/student`로 토큰 자동 갱신

신규 코드는 항상 `authApi` / `publicApi` 헬퍼를 사용하세요. `api` (default export)는 deprecated입니다.

### 상태 관리
Redux/Zustand 미사용. React Context 사용:
- `SubscribedClubsProvider` (`contexts/subscribed-clubs-context.tsx`) — 구독 동아리 ID 목록, 구독 토글, 서버 동기화
- `MixpanelProvider` (`contexts/mixpanel-context.tsx`) — 애널리틱스

### UI 레이어 패턴 (`ui/`)
`ui/` 하위 기능별 폴더 구조:
- `hook/` — 데이터 페칭 훅 (예: `useClubs`, `useSubscribedClubs`)
- `model/` — 파생 상태 / 데이터 변환
- `components/` — 기능별 컴포넌트
- `index.ts` — barrel export

### 디자인 시스템 (constants/theme.ts)
`@/constants/theme`에서 임포트:
- `MainColors` — 오렌지 계열 팔레트 (`main` = `#FF5414`)
- `TagColors` — 카테고리별 색상 (봉사/학술/종교/취미교양/운동/공연)
- `Spacing` — 4px 기준 스케일: `xs`(4) `sm`(8) `md`(16) `lg`(24) `xl`(32) `xxl`(40) `xxxl`(48)
- `BorderRadius` — `xs`(4) `sm`(8) `md`(12) `lg`(16) `xl`(20) `full`(9999)

폰트: **Pretendard** (Regular/Medium/SemiBold/Bold). React Native의 `Text` 대신 `@/components/moa-text`의 `<Text type="...">` 사용.

타이포그래피 변형: `heading1-3`, `title1-3`, `body1SemiBold`, `body1Medium`, `body1Regular`, `body2Regular`, `caption1SemiBold`, `caption1Medium`.

### 네이밍 컨벤션
- 파일명: `kebab-case.tsx`
- 컴포넌트: `PascalCase`
- 훅: `use` 접두사 + `camelCase`
- named export 선호; default export는 `app/` 하위 화면 컴포넌트에만 사용
- 경로 별칭: `@/`는 프로젝트 루트를 가리킴

### 플랫폼별 파일
플랫폼 오버라이드는 `.ios.tsx` / `.web.ts` 접미사 사용 (예: `icon-symbol.ios.tsx`, `use-color-scheme.web.ts`).
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gem "fastlane", ">= 2.228", "< 3.0"
Loading
Loading