docs+build(itms): CI/CD 정본 문서·APK 게시 자산·모바일 빌드 수정 (v1.0.0 APK 게시)

- docs/CICD_PIPELINE.md 정본(LIVE) + deploy/README.md 가동중 갱신
- deploy/apk/: publish_app_manifest.sh·mobile-app.html(WISE QR)·itms-latest.json
- deploy/nginx: /mobile-app(ITMS 전용)+/downloads/(APK)+매니페스트 서빙 반영
- mobile: EAS 빌드 의존 승격(react-navigation/expo-modules-core/metro)·.easignore·eas appVersionSource

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
itms-merge-dev 2026-07-19 19:41:29 +09:00
parent efc3e66635
commit 5e5ccc0ec7
14 changed files with 2089 additions and 196 deletions

View File

@ -1,7 +1,7 @@
# ITMS CI/CD 배포 아티팩트 (적용 대기) # ITMS CI/CD 배포 아티팩트
> 생성: 2026-07-17 (itms-cicd 하네스, 산출물 준비형) | 대상 저장소: Gitea `ythong/itms` > 생성: 2026-07-17 (산출물 준비형)**2026-07-19 적용 완료(LIVE)** | 대상 저장소: Gitea `ythong/itms`
> **상태: 적용 대기(NOT applied).** 이 디렉터리는 배포 대상 환경에서 적용할 준비된 산출물이다. 서버는 변경되지 않았다. > **상태: 가동 중.** 개발(itms.zioinfo.co.kr: front+auth+api 실로그인)·운영(itms.wise.ai.kr: front-only) 배포 완료, push 자동 연쇄(dev→prod) 동작. **현행 정본 문서는 `docs/CICD_PIPELINE.md`** — 아래 2026-07-17 기록은 당시 판정 이력으로 보존(피저빌리티 사유 중 JDK17 부재·DB 미도달은 이후 해소: JDK17 설치됨·dev MariaDB+역설계 스키마 적용).
## 왜 "적용 대기"인가 (피저빌리티 게이트) ## 왜 "적용 대기"인가 (피저빌리티 게이트)

23
deploy/apk/README.md Normal file
View File

@ -0,0 +1,23 @@
# ITMS 모바일 APK — 서버 게시 준비물
> 로컬 준비물만 포함(서버 무접촉). 서버 작업은 담당 에이전트가 수행.
## 파일
- `itms-latest.json` — 배포 매니페스트(버전·apkUrl·sizeBytes·sha256·publishedAt). `sizeBytes`/`sha256` 는 서버 배치 후 `publish_app_manifest.sh` 실행으로 확정.
- `publish_app_manifest.sh` — APK 배치 후 매니페스트를 원자적으로 기록(+qrencode 있으면 QR PNG 생성). CAMP `deploy/publish_app_manifest.sh` 패턴.
- `mobile-app.html``/mobile-app` 정적 게시 페이지(WISE 스타일, 다크모드 대응). `itms-latest.json` 을 fetch 해 버전/크기/SHA/다운로드 자동 표시. `itms-apk-qr.png` 존재 시 QR 표시, 없으면 다운로드 버튼 안내로 폴백.
## 서버 게시 절차(담당 에이전트용, 참고)
```bash
# 1) APK 배치
cp itms-mobile-v1.0.0.apk /opt/itms/downloads/itms.apk
# 2) 매니페스트 + QR 생성(운영 도메인)
./publish_app_manifest.sh 1.0.0 /opt/itms/downloads/itms.apk https://itms.wise.ai.kr/downloads/itms.apk
# 3) 정적 페이지 배치
cp mobile-app.html /opt/itms/downloads/mobile-app.html # 또는 nginx location /mobile-app
# 4) nginx: /downloads/ 와 /mobile-app 정적 서빙 + (필요 시) itms.apk MIME application/vnd.android.package-archive
```
## 주의
- EXPO 토큰·키스토어·자격증명 미포함. 공개 다운로드 URL만 기록.
- 다운로드 URL 기본값은 운영(`itms.wise.ai.kr`). 개발 검증은 `https://itms.zioinfo.co.kr/downloads/itms.apk` 등으로 3번째 인자 지정.

View File

@ -0,0 +1,13 @@
{
"appName": "ITMS 유지보수관리 모바일 앱",
"version": "1.0.0",
"platform": "ANDROID",
"apkUrl": "https://itms.wise.ai.kr/downloads/itms.apk",
"downloadUrl": "https://itms.wise.ai.kr/downloads/itms.apk",
"sizeBytes": 64069515,
"fileSizeMb": 61.1,
"sha256": "8421fa2a479ec6c8387eeca326910fc684975d70cdbe7c6eea4a541305451d8d",
"publishedAt": "2026-07-19",
"builtAt": "2026-07-19",
"notes": "EAS build e37c6626-9aba-4b76-b616-862f48f1cc9c (preview 프로파일, APK). 서버 배치 후 publish_app_manifest.sh 로 sizeBytes/sha256 재확정."
}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ITMS 모바일 앱 설치</title>
<style>
:root{ --brand:#1f29fc; --accent:#11c3ff; --bg:#f4f6f9; --card:#fff; --text:#252525; --muted:#6b7280; --border:#e6eaf0; }
*{ box-sizing:border-box; }
body{ margin:0; font-family:'Pretendard',system-ui,-apple-system,'Malgun Gothic',sans-serif; background:var(--bg); color:var(--text); }
.wrap{ max-width:520px; margin:0 auto; padding:32px 20px 56px; }
.hero{ background:linear-gradient(135deg,var(--accent),var(--brand)); border-radius:20px; padding:34px 24px; text-align:center; color:#fff; }
.hero h1{ margin:0; font-size:34px; font-weight:900; letter-spacing:3px; }
.hero p{ margin:8px 0 0; font-size:14px; opacity:.92; }
.card{ background:var(--card); border:1px solid var(--border); border-radius:16px; padding:22px; margin-top:18px; }
.qr{ display:flex; flex-direction:column; align-items:center; gap:12px; }
.qr img{ width:220px; height:220px; border-radius:12px; background:#fff; padding:10px; border:1px solid var(--border); }
.qr .hint{ color:var(--muted); font-size:13px; text-align:center; }
.cta{ display:block; text-align:center; text-decoration:none; background:var(--brand); color:#fff; font-weight:800; font-size:16px; padding:15px; border-radius:14px; margin-top:16px; }
.meta{ margin-top:14px; }
.row{ display:flex; justify-content:space-between; padding:9px 0; border-bottom:1px solid var(--border); font-size:14px; }
.row .k{ color:var(--muted); } .row .v{ font-weight:600; word-break:break-all; text-align:right; margin-left:12px; }
.sha{ font-size:11px; color:var(--muted); word-break:break-all; margin-top:8px; }
.note{ color:var(--muted); font-size:12px; line-height:1.7; margin-top:18px; }
.steps{ counter-reset:s; padding:0; margin:12px 0 0; list-style:none; }
.steps li{ counter-increment:s; position:relative; padding:6px 0 6px 30px; font-size:13px; color:var(--text); }
.steps li::before{ content:counter(s); position:absolute; left:0; top:5px; width:20px; height:20px; border-radius:50%; background:var(--brand); color:#fff; font-size:12px; font-weight:800; display:flex; align-items:center; justify-content:center; }
@media (prefers-color-scheme: dark){
:root{ --bg:#0b0f17; --card:#141a26; --text:#e6edf3; --muted:#8b97a8; --border:#283142; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="hero">
<h1>ITMS</h1>
<p>IT 유지보수관리 모바일 앱</p>
</div>
<div class="card qr">
<img id="qr" src="itms-apk-qr.png" alt="설치 QR" onerror="this.style.display='none';document.getElementById('qrHint').textContent='아래 다운로드 버튼으로 설치하세요. (QR 이미지는 서버에서 생성)';" />
<div class="hint" id="qrHint">카메라로 QR을 스캔하면 앱 다운로드가 시작됩니다.</div>
<a class="cta" id="dl" href="#">APK 다운로드</a>
</div>
<div class="card">
<div class="meta">
<div class="row"><span class="k"></span><span class="v" id="appName">ITMS 유지보수관리 모바일 앱</span></div>
<div class="row"><span class="k">버전</span><span class="v" id="version">-</span></div>
<div class="row"><span class="k">플랫폼</span><span class="v" id="platform">Android</span></div>
<div class="row"><span class="k">크기</span><span class="v" id="size">-</span></div>
<div class="row"><span class="k">배포일</span><span class="v" id="publishedAt">-</span></div>
</div>
<div class="sha" id="sha"></div>
</div>
<div class="card">
<strong>설치 방법</strong>
<ol class="steps">
<li>QR 스캔 또는 다운로드 버튼으로 APK를 내려받습니다.</li>
<li>Android 설정에서 '출처를 알 수 없는 앱 설치'를 허용합니다.</li>
<li>내려받은 APK를 실행해 설치합니다.</li>
<li>ITMS 계정(아이디/비밀번호)으로 로그인합니다.</li>
</ol>
<p class="note">사내/기관 내부 배포용입니다. iOS는 별도 배포 채널을 통해 제공됩니다.</p>
</div>
</div>
<script>
// itms-latest.json(매니페스트)을 읽어 화면 최신화. 백엔드 공개 엔드포인트로 대체 가능(fetch 경로만 변경).
(function(){
fetch('itms-latest.json', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(function(m){
var $ = function(id){ return document.getElementById(id); };
if (m.appName) $('appName').textContent = m.appName;
$('version').textContent = m.version || '-';
$('platform').textContent = m.platform || 'Android';
var mb = m.fileSizeMb; $('size').textContent = (mb && mb !== null) ? (mb + ' MB') : '-';
$('publishedAt').textContent = m.publishedAt || m.builtAt || '-';
var url = m.apkUrl || m.downloadUrl || '#';
$('dl').setAttribute('href', url);
if (m.sha256) $('sha').textContent = 'SHA-256: ' + m.sha256;
})
.catch(function(){ /* 매니페스트 없으면 정적 기본값 유지 */ });
})();
</script>
</body>
</html>

View File

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# ITMS 모바일 앱 배포 매니페스트 publish — APK 산출 후 1회 실행(웹 /mobile-app 자동 최신화).
# · 역할: 서버 APK 옆에 itms-latest.json(버전·다운로드URL·빌드일자·크기·sha256)을 원자적으로 기록.
# /mobile-app 페이지(또는 백엔드 공개 엔드포인트)가 이 파일을 읽어 QR/다운로드를 최신화.
# · 보안: 자격증명·키스토어·EXPO 토큰 미취급. 공개 다운로드 URL만 기록.
# · CAMP deploy/publish_app_manifest.sh 패턴 이식(솔루션=ITMS, 포트/도메인만 상이).
#
# 사용: publish_app_manifest.sh <version> <apk_path> [download_url]
# 예) cp itms-mobile-v1.0.0.apk /opt/itms/downloads/itms.apk
# publish_app_manifest.sh 1.0.0 /opt/itms/downloads/itms.apk https://itms.wise.ai.kr/downloads/itms.apk
set -euo pipefail
VERSION="${1:?usage: publish_app_manifest.sh <version> <apk_path> [download_url]}"
APK_PATH="${2:?usage: publish_app_manifest.sh <version> <apk_path> [download_url]}"
DOWNLOAD_URL="${3:-${ITMS_APP_DOWNLOAD_URL:-https://itms.wise.ai.kr/downloads/itms.apk}}"
MANIFEST="${ITMS_APP_MANIFEST:-/opt/itms/downloads/itms-latest.json}"
APP_NAME="${ITMS_APP_NAME:-ITMS 유지보수관리 모바일 앱}"
PLATFORM="${ITMS_APP_PLATFORM:-ANDROID}"
BUILT_AT="$(date +%Y-%m-%d)"
SIZE_BYTES="0"; SIZE_MB="null"; SHA256="null"
if [ -f "$APK_PATH" ]; then
SIZE_BYTES=$(stat -c%s "$APK_PATH" 2>/dev/null || wc -c < "$APK_PATH")
SIZE_MB=$(awk -v b="$SIZE_BYTES" 'BEGIN { printf "%.1f", b/1048576 }')
HASH=$(sha256sum "$APK_PATH" 2>/dev/null | awk '{print $1}')
[ -n "$HASH" ] && SHA256="\"${HASH}\""
fi
mkdir -p "$(dirname "$MANIFEST")"
TMP="${MANIFEST}.tmp.$$"
cat > "$TMP" <<JSON
{
"appName": "${APP_NAME}",
"version": "${VERSION}",
"platform": "${PLATFORM}",
"apkUrl": "${DOWNLOAD_URL}",
"downloadUrl": "${DOWNLOAD_URL}",
"sizeBytes": ${SIZE_BYTES},
"fileSizeMb": ${SIZE_MB},
"sha256": ${SHA256},
"publishedAt": "${BUILT_AT}",
"builtAt": "${BUILT_AT}",
"notes": null
}
JSON
mv -f "$TMP" "$MANIFEST"
# (선택) /mobile-app 정적 페이지 옆 QR 이미지 생성 — qrencode 설치 시.
if command -v qrencode >/dev/null 2>&1; then
qrencode -o "$(dirname "$MANIFEST")/itms-apk-qr.png" -s 8 "$DOWNLOAD_URL" || true
echo "[publish] QR -> $(dirname "$MANIFEST")/itms-apk-qr.png"
fi
echo "[publish] manifest -> $MANIFEST"
echo "[publish] version=$VERSION platform=$PLATFORM publishedAt=$BUILT_AT sizeBytes=$SIZE_BYTES sizeMb=$SIZE_MB"
echo "[publish] apkUrl=$DOWNLOAD_URL"

View File

@ -14,11 +14,25 @@ server {
server_name itms.wise.ai.kr; server_name itms.wise.ai.kr;
client_max_body_size 50m; client_max_body_size 50m;
# 통합 메신저 앱 QR 다운로드 페이지(읽기전용, 중앙 ITSM APK 저장소 재사용) # ITMS 전용 모바일 앱 설치 페이지(WISE QR — itms-latest.json fetch)
location = /mobile-app { location = /mobile-app {
alias /opt/itms/static/mobile-app.html; alias /opt/itms/downloads/mobile-app.html;
default_type text/html; default_type text/html;
} }
# APK 게시 자산 (매니페스트/QR/다운로드) — dev→prod tar-over-ssh 배치 후 서빙
location = /itms-latest.json {
default_type application/json;
alias /opt/itms/downloads/itms-latest.json;
}
location = /itms-apk-qr.png {
alias /opt/itms/downloads/itms-apk-qr.png;
}
location /downloads/ {
alias /opt/itms/downloads/;
types { application/vnd.android.package-archive apk; }
default_type application/octet-stream;
add_header Content-Disposition "attachment";
}
location / { location / {
proxy_pass http://127.0.0.1:11020; proxy_pass http://127.0.0.1:11020;

View File

@ -36,11 +36,25 @@ server {
client_max_body_size 50m; client_max_body_size 50m;
# 통합 메신저 앱 QR 다운로드 페이지(읽기전용, 중앙 ITSM APK 저장소 재사용) # ITMS 전용 모바일 앱 설치 페이지(WISE QR — itms-latest.json fetch)
location = /mobile-app { location = /mobile-app {
alias /opt/itms/static/mobile-app.html; alias /opt/itms/downloads/mobile-app.html;
default_type text/html; default_type text/html;
} }
# APK 게시 자산 (매니페스트/QR/다운로드) — publish_app_manifest.sh 로 갱신
location = /itms-latest.json {
default_type application/json;
alias /opt/itms/downloads/itms-latest.json;
}
location = /itms-apk-qr.png {
alias /opt/itms/downloads/itms-apk-qr.png;
}
location /downloads/ {
alias /opt/itms/downloads/;
types { application/vnd.android.package-archive apk; }
default_type application/octet-stream;
add_header Content-Disposition "attachment";
}
# OAuth2 인가서버(:11000) — 모바일 password/refresh grant # OAuth2 인가서버(:11000) — 모바일 password/refresh grant
location /oauth/ { location /oauth/ {

73
docs/CICD_PIPELINE.md Normal file
View File

@ -0,0 +1,73 @@
# ITMS CI/CD 파이프라인 정본 (LIVE)
> 최종 갱신: 2026-07-19 | 상태: **가동 중** (2026-07-17 "산출물 준비형" 판정을 대체)
> 상세 실행 기록: `C:\GUARDiA\_workspace\itms_cicd\01~06_*.md` (설계·구축·스키마·모바일·SR·QA)
## 1. 한 줄 요약
**`git push` 한 번이면 개발서버 빌드·배포 → 헬스체크 → 운영서버 자동 승격 → QR 페이지까지 자동 처리된다.**
```
git push (Gitea ythong/itms, main)
→ Gitea webhook → deploy_server.py itms 블록 (개발서버 101.79.17.164:9999)
→ gradle 모듈 빌드 (common 선행 → auth/api/front/batch/datatrans bootWar, JDK17)
→ 산출물 아카이브 (/opt/itms/artifacts/current)
→ dev 재기동: itms-front + itms-auth + itms-api (systemd)
→ dev 헬스체크 게이트 ← 실패 시 여기서 중단 (운영 오염 방지)
→ prod 승격: front WAR tar-over-ssh (ssh -p 9271, jenkins 배포키)
→ prod 재기동 + 헬스체크 → 완료
```
## 2. 서버·도메인
| 구분 | 서버 | 도메인 | 기동 범위 | TLS |
|------|------|--------|----------|-----|
| 개발 | 101.79.17.164 | https://itms.zioinfo.co.kr | front(11020)+auth(11000)+api(11010) — **실로그인 동작** | Let's Encrypt(자동갱신) |
| 운영 | 211.37.173.197 (wise.ai.kr, ssh :9271) | https://itms.wise.ai.kr | **front-only** (RAM 게이트) | Let's Encrypt(자동갱신) |
- 운영서버 211.37.173.197 ITMS 배포는 **2026-07-19 소유자 지정·승인** (CAMP와 동일 서버, Host 헤더 기반 vhost — bare IP 접근은 404).
- **운영 RAM 게이트**: prod 총 2GB·가용 ~500MB → front 단일 JVM(-Xmx320m)만. auth/api/DB 운영 기동은 **RAM 증설 후** (그 전 강행 금지 — 기존 WISE/CAMP 운영 서비스 보호).
## 3. 개발서버 런타임 (Stage B — 로그인 실동작)
- MariaDB 10.11 + `ur_itms_db` (전용 계정 `itms_zio`) + swap 2G
- 스키마: 고객 DDL 부재 → **MyBatis 매퍼 역설계 22테이블** (`_workspace/itms_cicd/03_schema_min.sql`)
- 함정1: auth는 `JdbcTokenStore``oauth_access_token`/`oauth_refresh_token`/`oauth_approvals` 3테이블 없으면 로그인 자체 실패
- 함정2: 로그인 쿼리 = `COMVNUSERMASTER ⋈ TB_USER_CRYP_CNF` INNER JOIN → 두 테이블에 동일 `ESNTL_ID` 시드 필수
- 프로파일: 기존 16기관 yml **불변**. 신규 `zio` 프로파일은 서버 외부 config(`/opt/itms/config/application-zio.yml`, `-Dspring.config.additional-location`) — 재빌드 불필요
- 시크릿: `/opt/itms/secrets/` (0600) — 데모 admin 비번·DB 비번. 문서/커밋 기재 금지
- **nginx Bearer 분기(핵심 함정)**: front와 api가 동일 URL prefix(/main·/srm·/sys 등)를 화면/REST로 각각 사용 → 경로 분기 불가. `Authorization: Bearer` 헤더 map으로 분기: Bearer→api(11010), 세션(브라우저)→front(11020), `/oauth/`→auth(11000). 정본: `deploy/nginx/itms.zioinfo.co.kr.conf`
## 4. 모바일 앱 · QR 배포
- 앱: `mobile/` 독립 Expo 앱(SDK 51, TS, 12화면) — Gradle 컴포짓 무관 순수 Node
- 인증: OAuth2 password grant `POST /oauth/token`(+Basic client, `ssoYn=N`) → RS256 JWT 24h
- 업무 API: 루트 prefix `POST /*.do` + Bearer (`/api` prefix 아님 — `constants/Config.ts` 권위), 응답 봉투 `{data,status,totalCount,code,message}`
- QR 다운로드 페이지: dev·prod `/mobile-app` — APK 매니페스트 기반 버전·QR·다운로드 표시
- APK: EAS 클라우드 빌드(EXPO_TOKEN 환경변수 — 값 저장 금지)
## 5. 고객 SR 반영 현황 (2026-07-19 push 53b8f42c)
- 원본: `docs/ITSM 고객 수정 요청 체크리스트_20260708.xlsx`(25건) + `docs/ITSM 기능개선 및 장애처리_V1.1_20260707.pptx`(15건) + 브랜딩(F12) = 40건
- **완료 23건** (로그인 잠금/메시지, 서비스데스크 서버측 권한·검증, 용어 통일 172건, 임시저장 버튼, 로고/파비콘 관리 등) — 개발·운영 라이브 검증 완료
- **보류 17건**: 고객 DB 스키마/프로시저 의존·설계 확정 필요·정책 판단 — `docs/SR_BACKLOG_20260708.md` 권고안 포함
- 조치 기록(고객 제출용): `docs/ITSM 고객 수정 요청 체크리스트_20260708_조치결과.xlsx` — 쉬운 설명 + **변경 전/후 코드 나란히 비교**
- 상세 diff: `docs/SR_CHANGES_20260719.md`
## 6. 운영 절차 (Runbook)
| 작업 | 방법 |
|------|------|
| 배포 | `git push origin main` — 이후 전자동. **push = 운영 배포**이므로 로컬 빌드 게이트(전 모듈 bootWar EXIT 0) 통과 후에만 |
| 배포 추적 | dev `journalctl -u zioinfo-deploy -f` (webhook→build→restart→promote 로그) |
| 롤백 | dev: `/opt/itms/artifacts/` 이전 아카이브 재기동 / prod: 이전 front WAR 재전송+재기동 |
| 헬스 | dev/prod 도메인 HTTP 200 + `systemctl status itms-front itms-auth itms-api` |
| deploy_server 수정 | 로컬 정본 `scripts/deploy/deploy_server.py` + 서버 `/opt/zioinfo/deploy_server.py` **양쪽 반영 + zioinfo-deploy 재시작** 필수(미반영 시 웹훅 1ms no-op) |
## 7. 잔여 백로그
1. APK 게시: EAS 빌드 완료 → dev/prod `/mobile-app` 매니페스트 갱신 (진행 중)
2. 운영 풀스택(로그인): **RAM 증설 결정 대기**
3. DB 의존 SR 17건: 고객 `ur_itms_db` 덤프 확보 시 (또는 역설계 확장)
4. batch(Oracle XE)·datatrans(기관 NAS): 기관 전용망 확보 시 — CI 빌드·아카이브는 이미 포함
5. legacy/nlib: CI 제외 유지(Maven·Java8)

13
mobile/.easignore Normal file
View File

@ -0,0 +1,13 @@
android/
ios/
node_modules/
.git/
.expo/
dist/
*.apk
*.log
build_final.json
logurl.txt
gradle2.txt
scripts/gen_icon.js
poll_build.sh

View File

@ -66,7 +66,7 @@ module.exports = {
itmsOauthClientId: process.env.ITMS_OAUTH_CLIENT_ID || 'clientId', itmsOauthClientId: process.env.ITMS_OAUTH_CLIENT_ID || 'clientId',
itmsOauthClientSecret: process.env.ITMS_OAUTH_CLIENT_SECRET || 'secretKey', itmsOauthClientSecret: process.env.ITMS_OAUTH_CLIENT_SECRET || 'secretKey',
router: { origin: false }, router: { origin: false },
eas: { projectId: process.env.ITMS_EAS_PROJECT_ID || undefined }, eas: { projectId: 'c26343b8-c8af-40bf-856e-9fb6a1df2bef' },
}, },
owner: 'infraurp', owner: 'infraurp',
}, },

View File

@ -1,6 +1,7 @@
{ {
"cli": { "cli": {
"version": ">= 10.0.0" "version": ">= 10.0.0",
"appVersionSource": "local"
}, },
"build": { "build": {
"development": { "development": {

1872
mobile/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,15 @@
"@expo/metro-runtime": "~3.2.3", "@expo/metro-runtime": "~3.2.3",
"@expo/vector-icons": "^14.0.3", "@expo/vector-icons": "^14.0.3",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/core": "^6.4.17",
"@react-navigation/native": "^6.1.18",
"axios": "^1.7.7", "axios": "^1.7.7",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"expo": "^51.0.39", "expo": "^51.0.39",
"expo-constants": "~16.0.2", "expo-constants": "~16.0.2",
"expo-font": "~12.0.10", "expo-font": "~12.0.10",
"expo-linking": "~6.3.1", "expo-linking": "~6.3.1",
"expo-modules-core": "~1.12.26",
"expo-router": "~3.5.23", "expo-router": "~3.5.23",
"expo-secure-store": "~13.0.2", "expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.7", "expo-splash-screen": "~0.27.7",
@ -33,12 +36,11 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.0", "@babel/core": "^7.24.0",
"@expo/config-plugins": "~8.0.0",
"@types/base-64": "^1.0.2", "@types/base-64": "^1.0.2",
"@types/react": "~18.2.79", "@types/react": "~18.2.79",
"metro": "~0.80.12",
"typescript": "~5.3.3" "typescript": "~5.3.3"
}, },
"overrides": {
"expo-font": "~12.0.10"
},
"private": true "private": true
} }

View File

@ -1,90 +0,0 @@
/* 자체 PNG 생성기 — 외부 의존 없이 WISE 블루(#1f29fc) 배경 + 흰색 'ITMS' 아이콘. */
const zlib = require('zlib')
const fs = require('fs')
const path = require('path')
const SIZE = 1024
const BG = [0x1f, 0x29, 0xfc, 0xff] // WISE blue
const FG = [0xff, 0xff, 0xff, 0xff] // white
// 5x7 비트맵 글리프
const GLYPHS = {
I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
M: ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
S: ['01111', '10000', '10000', '01110', '00001', '00001', '11110'],
}
const WORD = 'ITMS'
function makeBuffer() {
const buf = Buffer.alloc(SIZE * SIZE * 4)
for (let i = 0; i < SIZE * SIZE; i++) buf.set(BG, i * 4)
// 글리프 레이아웃: 4글자 * 5px + 간격 1px = 23 units 폭, 7 units 높이
const cols = WORD.length * 5 + (WORD.length - 1) // 23
const rows = 7
const scale = Math.floor((SIZE * 0.62) / cols) // 폭 62% 사용
const wpx = cols * scale
const hpx = rows * scale
const ox = Math.floor((SIZE - wpx) / 2)
const oy = Math.floor((SIZE - hpx) / 2)
const setPx = (x, y) => {
if (x < 0 || y < 0 || x >= SIZE || y >= SIZE) return
buf.set(FG, (y * SIZE + x) * 4)
}
let cx = ox
for (const ch of WORD) {
const g = GLYPHS[ch]
for (let r = 0; r < 7; r++) {
for (let c = 0; c < 5; c++) {
if (g[r][c] === '1') {
for (let dy = 0; dy < scale; dy++)
for (let dx = 0; dx < scale; dx++)
setPx(cx + c * scale + dx, oy + r * scale + dy)
}
}
}
cx += 6 * scale // 5 글자폭 + 1 간격
}
return buf
}
function crc32(buf) {
let c = ~0
for (let i = 0; i < buf.length; i++) {
c ^= buf[i]
for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1))
}
return ~c >>> 0
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0)
const t = Buffer.from(type, 'ascii')
const body = Buffer.concat([t, data])
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(body), 0)
return Buffer.concat([len, body, crc])
}
function encodePng(rgba) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
const ihdr = Buffer.alloc(13)
ihdr.writeUInt32BE(SIZE, 0); ihdr.writeUInt32BE(SIZE, 4)
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0
// 각 스캔라인 앞 filter byte 0
const raw = Buffer.alloc(SIZE * (SIZE * 4 + 1))
for (let y = 0; y < SIZE; y++) {
raw[y * (SIZE * 4 + 1)] = 0
rgba.copy(raw, y * (SIZE * 4 + 1) + 1, y * SIZE * 4, (y + 1) * SIZE * 4)
}
const idat = zlib.deflateSync(raw, { level: 9 })
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))])
}
const png = encodePng(makeBuffer())
const outDir = path.join(__dirname, '..', 'assets')
fs.mkdirSync(outDir, { recursive: true })
for (const name of ['icon.png', 'adaptive-icon.png', 'splash.png']) {
fs.writeFileSync(path.join(outDir, name), png)
}
console.log('generated', png.length, 'bytes ->', outDir)